diff --git a/.gitignore b/.gitignore index 3975c4521..944fb8418 100644 --- a/.gitignore +++ b/.gitignore @@ -37,11 +37,12 @@ resources /omwlauncher /openmw /opencs +/niftest ## generated objects apps/openmw/config.hpp components/version/version.hpp -Docs/mainpage.hpp +docs/mainpage.hpp moc_*.cxx *.cxx_parameters *qrc_launcher.cxx diff --git a/.travis.yml b/.travis.yml index e09fa46dc..233117718 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,38 +1,25 @@ +os: + - linux + - osx language: cpp -compiler: - - gcc branches: only: - master - /openmw-.*$/ before_install: - - pwd - - echo "yes" | sudo add-apt-repository "deb http://archive.ubuntu.com/ubuntu `lsb_release -sc` main universe restricted multiverse" - - echo "yes" | sudo apt-add-repository ppa:openmw/openmw - - sudo apt-get update -qq - - sudo apt-get install -qq libboost-all-dev libgtest-dev google-mock - - sudo apt-get install -qq libqt4-dev - - sudo apt-get install -qq libopenal-dev - - sudo apt-get install -qq libavcodec-dev libavformat-dev libavutil-dev libswscale-dev - - sudo apt-get install -qq libbullet-dev libogre-1.9-dev libmygui-dev libsdl2-dev libunshield-dev - - sudo mkdir /usr/src/gtest/build - - cd /usr/src/gtest/build - - sudo cmake .. -DBUILD_SHARED_LIBS=1 - - sudo make -j4 - - sudo ln -s /usr/src/gtest/build/libgtest.so /usr/lib/libgtest.so - - sudo ln -s /usr/src/gtest/build/libgtest_main.so /usr/lib/libgtest_main.so + - if [ "${TRAVIS_OS_NAME}" = "linux" ]; then ./CI/before_install.linux.sh; fi + - if [ "${TRAVIS_OS_NAME}" = "osx" ]; then ./CI/before_install.osx.sh; fi before_script: - - cd - - - mkdir build - - cd build - - cmake .. -DBUILD_WITH_CODE_COVERAGE=1 -DBUILD_UNITTESTS=1 -DBUILD_WITH_DPKG=1 + - if [ "${TRAVIS_OS_NAME}" = "linux" ]; then ./CI/before_script.linux.sh; fi + - if [ "${TRAVIS_OS_NAME}" = "osx" ]; then ./CI/before_script.osx.sh; fi script: + - cd ./build - make -j4 after_script: - - ./openmw_test_suite + - if [ "${TRAVIS_OS_NAME}" = "linux" ]; then ./openmw_test_suite; fi notifications: recipients: - - lgromanowski+travis.ci@gmail.com + - corrmage+travis-ci@gmail.com email: on_success: change on_failure: always diff --git a/CI/before_install.linux.sh b/CI/before_install.linux.sh new file mode 100755 index 000000000..998c285db --- /dev/null +++ b/CI/before_install.linux.sh @@ -0,0 +1,18 @@ +#!/bin/sh + +export CXX=g++ +export CC=gcc + +echo "yes" | sudo add-apt-repository "deb http://archive.ubuntu.com/ubuntu `lsb_release -sc` main universe restricted multiverse" +echo "yes" | sudo apt-add-repository ppa:openmw/openmw +sudo apt-get update -qq +sudo apt-get install -qq libgtest-dev google-mock +sudo apt-get install -qq libboost-filesystem-dev libboost-program-options-dev libboost-system-dev libboost-thread-dev libboost-wave-dev +sudo apt-get install -qq libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libavresample-dev +sudo apt-get install -qq libbullet-dev libogre-1.9-dev libmygui-dev libsdl2-dev libunshield-dev libtinyxml-dev libopenal-dev libqt4-dev +sudo mkdir /usr/src/gtest/build +cd /usr/src/gtest/build +sudo cmake .. -DBUILD_SHARED_LIBS=1 +sudo make -j4 +sudo ln -s /usr/src/gtest/build/libgtest.so /usr/lib/libgtest.so +sudo ln -s /usr/src/gtest/build/libgtest_main.so /usr/lib/libgtest_main.so diff --git a/CI/before_install.osx.sh b/CI/before_install.osx.sh new file mode 100755 index 000000000..b1d4f991b --- /dev/null +++ b/CI/before_install.osx.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +export CXX=clang++ +export CC=clang + +brew tap openmw/openmw +brew update +brew unlink boost +brew install cmake openmw-mygui openmw-bullet openmw-sdl2 openmw-ffmpeg pkg-config qt unshield diff --git a/CI/before_script.linux.sh b/CI/before_script.linux.sh new file mode 100755 index 000000000..b4889c9e1 --- /dev/null +++ b/CI/before_script.linux.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +mkdir build +cd build +cmake .. -DBUILD_WITH_CODE_COVERAGE=1 -DBUILD_UNITTESTS=1 -DCMAKE_INSTALL_PREFIX=/usr -DBINDIR=/usr/games -DCMAKE_BUILD_TYPE="RelWithDebInfo" -DUSE_SYSTEM_TINYXML=TRUE diff --git a/CI/before_script.osx.sh b/CI/before_script.osx.sh new file mode 100755 index 000000000..772284f91 --- /dev/null +++ b/CI/before_script.osx.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +mkdir build +cd build +cmake -DCMAKE_FRAMEWORK_PATH="/usr/local/lib/macosx/Release" -DCMAKE_EXE_LINKER_FLAGS="-F/usr/local/lib/macosx/Release" -DCMAKE_CXX_FLAGS="-stdlib=libstdc++" -DCMAKE_BUILD_TYPE=Debug -DBUILD_MYGUI_PLUGIN=OFF -G"Unix Makefiles" .. diff --git a/CMakeLists.txt b/CMakeLists.txt index 66746b22c..84fc5043a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,8 +12,8 @@ set(CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake/) message(STATUS "Configuring OpenMW...") set(OPENMW_VERSION_MAJOR 0) -set(OPENMW_VERSION_MINOR 30) -set(OPENMW_VERSION_RELEASE 0) +set(OPENMW_VERSION_MINOR 33) +set(OPENMW_VERSION_RELEASE 1) set(OPENMW_VERSION_COMMITHASH "") set(OPENMW_VERSION_TAGHASH "") @@ -54,9 +54,13 @@ endif(EXISTS ${PROJECT_SOURCE_DIR}/.git) # Macros include(OpenMWMacros) +if (ANDROID) + set(CMAKE_FIND_ROOT_PATH ${OPENMW_DEPENDENCIES_DIR} "${CMAKE_FIND_ROOT_PATH}") +endif (ANDROID) + # doxygen main page -configure_file ("${OpenMW_SOURCE_DIR}/Docs/mainpage.hpp.cmake" "${OpenMW_SOURCE_DIR}/Docs/mainpage.hpp") +configure_file ("${OpenMW_SOURCE_DIR}/docs/mainpage.hpp.cmake" "${OpenMW_BINARY_DIR}/docs/mainpage.hpp") option(MYGUI_STATIC "Link static build of Mygui into the binaries" FALSE) option(OGRE_STATIC "Link static build of Ogre and Ogre Plugins into the binaries" FALSE) @@ -66,38 +70,30 @@ option(SDL2_STATIC "Link static build of SDL into the binaries" FALSE) option(OPENMW_UNITY_BUILD "Use fewer compilation units to speed up compile time" FALSE) # Apps and tools -option(BUILD_BSATOOL "build BSA extractor" OFF) +option(BUILD_BSATOOL "build BSA extractor" ON) option(BUILD_ESMTOOL "build ESM inspector" ON) option(BUILD_LAUNCHER "build Launcher" ON) option(BUILD_MWINIIMPORTER "build MWiniImporter" ON) option(BUILD_OPENCS "build OpenMW Construction Set" ON) option(BUILD_WIZARD "build Installation Wizard" ON) option(BUILD_WITH_CODE_COVERAGE "Enable code coverage with gconv" OFF) -option(BUILD_UNITTESTS "Enable Unittests with Google C++ Unittest ang GMock frameworks" OFF) - -# Sound source selection -option(USE_FFMPEG "use ffmpeg for sound" ON) +option(BUILD_UNITTESTS "Enable Unittests with Google C++ Unittest and GMock frameworks" OFF) +option(BUILD_NIFTEST "build nif file tester" OFF) +option(BUILD_MYGUI_PLUGIN "build MyGUI plugin for OpenMW resources, to use with MyGUI tools" ON) # OS X deployment option(OPENMW_OSX_DEPLOYMENT OFF) -if(UNIX AND NOT APPLE) - option(BUILD_WITH_DPKG "enable dpkg-based install for debian and debian derivatives" OFF) - if(BUILD_WITH_DPKG) - find_program(DPKG_PROGRAM dpkg DOC "dpkg program of Debian-based systems") - endif(BUILD_WITH_DPKG) -endif(UNIX AND NOT APPLE) - # Location of morrowind data files if (APPLE) set(MORROWIND_DATA_FILES "./data" CACHE PATH "location of Morrowind data files") - set(MORROWIND_RESOURCE_FILES "./resources" CACHE PATH "location of OpenMW resources files") + set(OPENMW_RESOURCE_FILES "./resources" CACHE PATH "location of OpenMW resources files") elseif(UNIX) - set(MORROWIND_DATA_FILES "/usr/share/games/openmw/data/" CACHE PATH "location of Morrowind data files") - set(MORROWIND_RESOURCE_FILES "/usr/share/games/openmw/resources/" CACHE PATH "location of OpenMW resources files") + set(MORROWIND_DATA_FILES "${CMAKE_INSTALL_PREFIX}/share/games/openmw/data/" CACHE PATH "location of Morrowind data files") + set(OPENMW_RESOURCE_FILES "${CMAKE_INSTALL_PREFIX}/share/games/openmw/resources/" CACHE PATH "location of OpenMW resources files") else() set(MORROWIND_DATA_FILES "data" CACHE PATH "location of Morrowind data files") - set(MORROWIND_RESOURCE_FILES "resources" CACHE PATH "location of OpenMW resources files") + set(OPENMW_RESOURCE_FILES "resources" CACHE PATH "location of OpenMW resources files") endif(APPLE) if (WIN32) @@ -109,33 +105,32 @@ cmake_minimum_required(VERSION 2.6) # source directory: libs -set(LIBDIR ${CMAKE_SOURCE_DIR}/libs) +set(LIBS_DIR ${CMAKE_SOURCE_DIR}/libs) set(OENGINE_OGRE - ${LIBDIR}/openengine/ogre/renderer.cpp - ${LIBDIR}/openengine/ogre/fader.cpp - ${LIBDIR}/openengine/ogre/lights.cpp - ${LIBDIR}/openengine/ogre/selectionbuffer.cpp - ${LIBDIR}/openengine/ogre/imagerotate.cpp + ${LIBS_DIR}/openengine/ogre/renderer.cpp + ${LIBS_DIR}/openengine/ogre/lights.cpp + ${LIBS_DIR}/openengine/ogre/selectionbuffer.cpp + ${LIBS_DIR}/openengine/ogre/imagerotate.cpp ) set(OENGINE_GUI - ${LIBDIR}/openengine/gui/loglistener.cpp - ${LIBDIR}/openengine/gui/manager.cpp - ${LIBDIR}/openengine/gui/layout.hpp + ${LIBS_DIR}/openengine/gui/loglistener.cpp + ${LIBS_DIR}/openengine/gui/manager.cpp + ${LIBS_DIR}/openengine/gui/layout.hpp ) set(OENGINE_BULLET - ${LIBDIR}/openengine/bullet/BtOgre.cpp - ${LIBDIR}/openengine/bullet/BtOgreExtras.h - ${LIBDIR}/openengine/bullet/BtOgreGP.h - ${LIBDIR}/openengine/bullet/BtOgrePG.h - ${LIBDIR}/openengine/bullet/physic.cpp - ${LIBDIR}/openengine/bullet/physic.hpp - ${LIBDIR}/openengine/bullet/BulletShapeLoader.cpp - ${LIBDIR}/openengine/bullet/BulletShapeLoader.h - ${LIBDIR}/openengine/bullet/trace.cpp - ${LIBDIR}/openengine/bullet/trace.h + ${LIBS_DIR}/openengine/bullet/BtOgre.cpp + ${LIBS_DIR}/openengine/bullet/BtOgreExtras.h + ${LIBS_DIR}/openengine/bullet/BtOgreGP.h + ${LIBS_DIR}/openengine/bullet/BtOgrePG.h + ${LIBS_DIR}/openengine/bullet/physic.cpp + ${LIBS_DIR}/openengine/bullet/physic.hpp + ${LIBS_DIR}/openengine/bullet/BulletShapeLoader.cpp + ${LIBS_DIR}/openengine/bullet/BulletShapeLoader.h + ${LIBS_DIR}/openengine/bullet/trace.cpp + ${LIBS_DIR}/openengine/bullet/trace.h ) @@ -146,32 +141,24 @@ set(OPENMW_LIBS ${OENGINE_ALL}) set(OPENMW_LIBS_HEADER) # Sound setup -set(GOT_SOUND_INPUT 0) -set(SOUND_INPUT_INCLUDES "") -set(SOUND_INPUT_LIBRARY "") -set(SOUND_DEFINE "") -if (USE_FFMPEG) - set(FFmpeg_FIND_COMPONENTS AVCODEC AVFORMAT AVUTIL SWSCALE) - find_package(FFmpeg) - if (FFMPEG_FOUND) - set(SOUND_INPUT_INCLUDES ${SOUND_INPUT_INCLUDES} ${FFMPEG_INCLUDE_DIRS}) - set(SOUND_INPUT_LIBRARY ${SOUND_INPUT_LIBRARY} ${FFMPEG_LIBRARIES} ${SWSCALE_LIBRARIES}) - set(SOUND_DEFINE ${SOUND_DEFINE} -DOPENMW_USE_FFMPEG) - set(GOT_SOUND_INPUT 1) - endif (FFMPEG_FOUND) -endif (USE_FFMPEG) - -if (NOT GOT_SOUND_INPUT) - message(WARNING "--------------------") - message(WARNING "Failed to find any sound input packages") - message(WARNING "--------------------") -endif (NOT GOT_SOUND_INPUT) - -if (NOT FFMPEG_FOUND) - message(WARNING "--------------------") - message(WARNING "FFmpeg not found, video playback will be disabled") - message(WARNING "--------------------") -endif (NOT FFMPEG_FOUND) +set(FFmpeg_FIND_COMPONENTS AVCODEC AVFORMAT AVUTIL SWSCALE SWRESAMPLE AVRESAMPLE) +unset(FFMPEG_LIBRARIES CACHE) +find_package(FFmpeg) +if ( NOT AVCODEC_FOUND OR NOT AVFORMAT_FOUND OR NOT AVUTIL_FOUND OR NOT SWSCALE_FOUND ) + message(FATAL_ERROR "FFmpeg component required, but not found!") +endif() +set(SOUND_INPUT_INCLUDES ${FFMPEG_INCLUDE_DIRS}) +set(SOUND_INPUT_LIBRARY ${FFMPEG_LIBRARIES} ${SWSCALE_LIBRARIES}) +if( SWRESAMPLE_FOUND ) + add_definitions(-DHAVE_LIBSWRESAMPLE) + set(SOUND_INPUT_LIBRARY ${FFMPEG_LIBRARIES} ${SWRESAMPLE_LIBRARIES}) +else() + if( AVRESAMPLE_FOUND ) + set(SOUND_INPUT_LIBRARY ${FFMPEG_LIBRARIES} ${AVRESAMPLE_LIBRARIES}) + else() + message(FATAL_ERROR "Install either libswresample (FFmpeg) or libavresample (Libav).") + endif() +endif() # TinyXML option(USE_SYSTEM_TINYXML "Use system TinyXML library instead of internal." OFF) @@ -235,32 +222,74 @@ IF(BOOST_STATIC) endif() find_package(OGRE REQUIRED) +if (${OGRE_VERSION} VERSION_LESS "1.9") + message(FATAL_ERROR "OpenMW requires Ogre 1.9 or later, please install the latest stable version from http://ogre3d.org") +endif() + find_package(MyGUI REQUIRED) +if (${MYGUI_VERSION} VERSION_LESS "3.2.1") + message(FATAL_ERROR "OpenMW requires MyGUI 3.2.1 or later, please install the latest version from http://mygui.info") +endif() + find_package(Boost REQUIRED COMPONENTS ${BOOST_COMPONENTS}) find_package(SDL2 REQUIRED) find_package(OpenAL REQUIRED) find_package(Bullet REQUIRED) -IF(OGRE_STATIC) -find_package(Cg) -IF(WIN32) -set(OGRE_PLUGIN_INCLUDE_DIRS ${OGRE_Plugin_CgProgramManager_INCLUDE_DIRS} ${OGRE_Plugin_OctreeSceneManager_INCLUDE_DIRS} ${OGRE_Plugin_ParticleFX_INCLUDE_DIRS} ${OGRE_RenderSystem_Direct3D9_INCLUDE_DIRS} ${OGRE_RenderSystem_GL_INCLUDE_DIRS}) -ELSE(WIN32) -set(OGRE_PLUGIN_INCLUDE_DIRS ${OGRE_Plugin_CgProgramManager_INCLUDE_DIRS} ${OGRE_Plugin_OctreeSceneManager_INCLUDE_DIRS} ${OGRE_Plugin_ParticleFX_INCLUDE_DIRS} ${OGRE_RenderSystem_GL_INCLUDE_DIRS}) -ENDIF(WIN32) -ENDIF(OGRE_STATIC) + +set(OGRE_PLUGIN_INCLUDE_DIRS "") +set(OGRE_STATIC_PLUGINS "") + +macro(add_static_ogre_plugin PLUGIN) + if(OGRE_${PLUGIN}_FOUND) + # strip RenderSystem_ or Plugin_ prefix from plugin name + string(REPLACE "RenderSystem_" "" PLUGIN_TEMP ${PLUGIN}) + string(REPLACE "Plugin_" "" PLUGIN_NAME ${PLUGIN_TEMP}) + add_definitions(-DENABLE_PLUGIN_${PLUGIN_NAME}) + + list(APPEND OGRE_PLUGIN_INCLUDE_DIRS ${OGRE_${PLUGIN}_INCLUDE_DIRS}) + list(APPEND OGRE_STATIC_PLUGINS ${OGRE_${PLUGIN}_LIBRARIES}) + endif(OGRE_${PLUGIN}_FOUND) +endmacro(add_static_ogre_plugin) + +if(OGRE_STATIC) + # set up OGRE_PLUGIN_INCLUDE_DIRS and OGRE_STATIC_PLUGINS + add_static_ogre_plugin(Plugin_OctreeSceneManager) + add_static_ogre_plugin(Plugin_ParticleFX) + find_package(Cg) + if(Cg_FOUND) + add_static_ogre_plugin(Plugin_CgProgramManager) + list(APPEND OGRE_STATIC_PLUGINS ${Cg_LIBRARIES}) + endif(Cg_FOUND) + +if (ANDROID) + add_static_ogre_plugin(RenderSystem_GLES2) +else () + add_static_ogre_plugin(RenderSystem_GL) +endif () + + if(WIN32) + add_static_ogre_plugin(RenderSystem_Direct3D9) + endif(WIN32) +endif(OGRE_STATIC) + include_directories("." - ${OGRE_INCLUDE_DIR} ${OGRE_INCLUDE_DIR}/Ogre ${OGRE_INCLUDE_DIR}/OGRE ${OGRE_PLUGIN_INCLUDE_DIRS} + ${OGRE_INCLUDE_DIR} ${OGRE_INCLUDE_DIR}/Ogre ${OGRE_INCLUDE_DIR}/OGRE ${OGRE_INCLUDE_DIRS} ${OGRE_PLUGIN_INCLUDE_DIRS} + ${OGRE_INCLUDE_DIR}/Overlay ${OGRE_Overlay_INCLUDE_DIR} ${SDL2_INCLUDE_DIR} ${Boost_INCLUDE_DIR} ${PLATFORM_INCLUDE_DIR} ${MYGUI_INCLUDE_DIRS} ${MYGUI_PLATFORM_INCLUDE_DIRS} ${OPENAL_INCLUDE_DIR} - ${LIBDIR} + ${LIBS_DIR} ) link_directories(${SDL2_LIBRARY_DIRS} ${Boost_LIBRARY_DIRS} ${OGRE_LIB_DIR} ${MYGUI_LIB_DIR}) +if(MYGUI_STATIC) + add_definitions(-DMYGUI_STATIC) +endif (MYGUI_STATIC) + if (APPLE) # List used Ogre plugins SET(USED_OGRE_PLUGINS ${OGRE_RenderSystem_GL_LIBRARY_REL} @@ -305,12 +334,14 @@ else () add_definitions(-DOGRE_PLUGIN_DEBUG_SUFFIX="_d") endif() -add_definitions(-DOGRE_PLUGIN_DIR_REL="${OGRE_PLUGIN_DIR_REL}") -add_definitions(-DOGRE_PLUGIN_DIR_DBG="${OGRE_PLUGIN_DIR_DBG}") if (APPLE AND OPENMW_OSX_DEPLOYMENT) # make it empty so plugin loading code can check this and try to find plugins inside app bundle add_definitions(-DOGRE_PLUGIN_DIR="") else() + if (NOT DEFINED ${OGRE_PLUGIN_DIR}) + set(OGRE_PLUGIN_DIR ${OGRE_PLUGIN_DIR_REL}) + endif() + add_definitions(-DOGRE_PLUGIN_DIR="${OGRE_PLUGIN_DIR}") endif() @@ -322,8 +353,10 @@ add_subdirectory(files/mygui) if (APPLE) set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${APP_BUNDLE_DIR}/Contents/MacOS") + set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${APP_BUNDLE_DIR}/Contents/MacOS") else (APPLE) set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${OpenMW_BINARY_DIR}") + set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${OpenMW_BINARY_DIR}") endif (APPLE) # Other files @@ -357,9 +390,6 @@ endif() if (CMAKE_COMPILER_IS_GNUCC) SET(CMAKE_CXX_FLAGS "-Wall -Wextra -Wno-unused-parameter -Wno-reorder -std=c++98 -pedantic -Wno-long-long ${CMAKE_CXX_FLAGS}") - # Silence warnings in OGRE headers. Remove once OGRE got fixed! - SET(CMAKE_CXX_FLAGS "-Wno-ignored-qualifiers ${CMAKE_CXX_FLAGS}") - execute_process(COMMAND ${CMAKE_C_COMPILER} -dumpversion OUTPUT_VARIABLE GCC_VERSION) if ("${GCC_VERSION}" VERSION_GREATER 4.6 OR "${GCC_VERSION}" VERSION_EQUAL 4.6) @@ -368,50 +398,52 @@ if (CMAKE_COMPILER_IS_GNUCC) endif (CMAKE_COMPILER_IS_GNUCC) IF(NOT WIN32 AND NOT APPLE) - ## Debian and non debian Linux building + # Linux building # Paths - IF (DPKG_PROGRAM) - ## Debian specific - SET(CMAKE_INSTALL_PREFIX "/usr") - SET(DATAROOTDIR "share" CACHE PATH "Sets the root of data directories to a non-default location") - SET(DATADIR "share/games/openmw" CACHE PATH "Sets the openmw data directories to a non-default location") - SET(ICONDIR "share/pixmaps" CACHE PATH "Set icon dir") - SET(SYSCONFDIR "../etc/openmw" CACHE PATH "Set config dir") - ELSE () - ## Non debian specific - SET(BINDIR "${CMAKE_INSTALL_PREFIX}/bin" CACHE PATH "Where to install binaries") - SET(DATAROOTDIR "${CMAKE_INSTALL_PREFIX}/share" CACHE PATH "Sets the root of data directories to a non-default location") - SET(DATADIR "${DATAROOTDIR}/games/openmw" CACHE PATH "Sets the openmw data directories to a non-default location") - SET(ICONDIR "${DATAROOTDIR}/pixmaps" CACHE PATH "Set icon dir") - SET(LICDIR "${DATAROOTDIR}/licenses/openmw" CACHE PATH "Sets the openmw license directory to a non-default location.") - SET(SYSCONFDIR "/etc/openmw" CACHE PATH "Set config dir") - - # Install binaries - INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/openmw" DESTINATION "${BINDIR}" ) - IF(BUILD_LAUNCHER) - INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/omwlauncher" DESTINATION "${BINDIR}" ) - ENDIF(BUILD_LAUNCHER) - IF(BUILD_BSATOOL) - INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/bsatool" DESTINATION "${BINDIR}" ) - ENDIF(BUILD_BSATOOL) - IF(BUILD_ESMTOOL) - INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/esmtool" DESTINATION "${BINDIR}" ) - ENDIF(BUILD_ESMTOOL) - IF(BUILD_MWINIIMPORTER) - INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/mwiniimport" DESTINATION "${BINDIR}" ) - ENDIF(BUILD_MWINIIMPORTER) - IF(BUILD_OPENCS) - INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/opencs" DESTINATION "${BINDIR}" ) - ENDIF(BUILD_OPENCS) - IF(BUILD_WIZARD) - INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/wizard" DESTINATION "${BINDIR}" ) - ENDIF(BUILD_WIZARD) - - - # Install licenses - INSTALL(FILES "DejaVu Font License.txt" DESTINATION "${LICDIR}" ) - INSTALL(FILES "extern/shiny/License.txt" DESTINATION "${LICDIR}" RENAME "Shiny License.txt" ) - ENDIF (DPKG_PROGRAM) + SET(BINDIR "${CMAKE_INSTALL_PREFIX}/bin" CACHE PATH "Where to install binaries") + SET(LIBDIR "${CMAKE_INSTALL_PREFIX}/lib" CACHE PATH "Where to install libraries") + SET(DATAROOTDIR "${CMAKE_INSTALL_PREFIX}/share" CACHE PATH "Sets the root of data directories to a non-default location") + SET(GLOBAL_DATA_PATH "${DATAROOTDIR}/games/" CACHE PATH "Set data path prefix") + SET(DATADIR "${GLOBAL_DATA_PATH}/openmw" CACHE PATH "Sets the openmw data directories to a non-default location") + SET(ICONDIR "${DATAROOTDIR}/pixmaps" CACHE PATH "Set icon dir") + SET(LICDIR "${DATAROOTDIR}/licenses/openmw" CACHE PATH "Sets the openmw license directory to a non-default location.") + IF("${CMAKE_INSTALL_PREFIX}" STREQUAL "/usr") + SET(GLOBAL_CONFIG_PATH "/etc/" CACHE PATH "Set config dir prefix") + ELSE() + SET(GLOBAL_CONFIG_PATH "${CMAKE_INSTALL_PREFIX}/etc/" CACHE PATH "Set config dir prefix") + ENDIF() + SET(SYSCONFDIR "${GLOBAL_CONFIG_PATH}/openmw" CACHE PATH "Set config dir") + + # Install binaries + INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/openmw" DESTINATION "${BINDIR}" ) + IF(BUILD_LAUNCHER) + INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/omwlauncher" DESTINATION "${BINDIR}" ) + ENDIF(BUILD_LAUNCHER) + IF(BUILD_BSATOOL) + INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/bsatool" DESTINATION "${BINDIR}" ) + ENDIF(BUILD_BSATOOL) + IF(BUILD_ESMTOOL) + INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/esmtool" DESTINATION "${BINDIR}" ) + ENDIF(BUILD_ESMTOOL) + IF(BUILD_MWINIIMPORTER) + INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/mwiniimport" DESTINATION "${BINDIR}" ) + ENDIF(BUILD_MWINIIMPORTER) + IF(BUILD_OPENCS) + INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/opencs" DESTINATION "${BINDIR}" ) + ENDIF(BUILD_OPENCS) + IF(BUILD_NIFTEST) + INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/niftest" DESTINATION "${BINDIR}" ) + ENDIF(BUILD_NIFTEST) + IF(BUILD_WIZARD) + INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/wizard" DESTINATION "${BINDIR}" ) + ENDIF(BUILD_WIZARD) + if(BUILD_MYGUI_PLUGIN) + INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/Plugin_MyGUI_OpenMW_Resources.so" DESTINATION "${LIBDIR}" ) + ENDIF(BUILD_MYGUI_PLUGIN) + + # Install licenses + INSTALL(FILES "docs/license/DejaVu Font License.txt" DESTINATION "${LICDIR}" ) + INSTALL(FILES "extern/shiny/License.txt" DESTINATION "${LICDIR}" RENAME "Shiny License.txt" ) # Install icon and desktop file INSTALL(FILES "${OpenMW_BINARY_DIR}/openmw.desktop" DESTINATION "${DATAROOTDIR}/applications" PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ COMPONENT "openmw") @@ -440,8 +472,8 @@ if(WIN32) INSTALL(FILES "${OpenMW_BINARY_DIR}/openmw.cfg.install" DESTINATION "." RENAME "openmw.cfg") INSTALL(FILES "${OpenMW_SOURCE_DIR}/readme.txt" - "${OpenMW_SOURCE_DIR}/GPL3.txt" - "${OpenMW_SOURCE_DIR}/DejaVu Font License.txt" + "${OpenMW_SOURCE_DIR}/Docs/license/GPL3.txt" + "${OpenMW_SOURCE_DIR}/Docs/license/DejaVu Font License.txt" "${OpenMW_BINARY_DIR}/settings-default.cfg" "${OpenMW_BINARY_DIR}/transparency-overrides.cfg" "${OpenMW_BINARY_DIR}/Release/openmw.exe" @@ -460,7 +492,9 @@ if(WIN32) IF(BUILD_WIZARD) INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/Release/openmw-wizard.exe" DESTINATION ".") ENDIF(BUILD_WIZARD) - + if(BUILD_MYGUI_PLUGIN) + INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/Release/Plugin_MyGUI_OpenMW_Resources.dll" DESTINATION ".") + ENDIF(BUILD_MYGUI_PLUGIN) INSTALL(DIRECTORY "${OpenMW_BINARY_DIR}/resources" DESTINATION ".") @@ -493,8 +527,8 @@ if(WIN32) SET(CPACK_NSIS_HELP_LINK "http:\\\\\\\\www.openmw.org") SET(CPACK_NSIS_URL_INFO_ABOUT "http:\\\\\\\\www.openmw.org") SET(CPACK_NSIS_INSTALLED_ICON_NAME "omwlauncher.exe") - SET(CPACK_NSIS_MUI_ICON "${OpenMW_SOURCE_DIR}/files/launcher/images/openmw.ico") - SET(CPACK_NSIS_MUI_UNIICON "${OpenMW_SOURCE_DIR}/files/launcher/images/openmw.ico") + SET(CPACK_NSIS_MUI_ICON "${OpenMW_SOURCE_DIR}/files/windows/openmw.ico") + SET(CPACK_NSIS_MUI_UNIICON "${OpenMW_SOURCE_DIR}/files/windows/openmw.ico") SET(CPACK_PACKAGE_ICON "${OpenMW_SOURCE_DIR}\\\\files\\\\openmw.bmp") SET(VCREDIST32 "${OpenMW_BINARY_DIR}/vcredist_x86.exe") @@ -525,12 +559,23 @@ endif(WIN32) # Extern add_subdirectory (extern/shiny) +add_subdirectory (extern/ogre-ffmpeg-videoplayer) add_subdirectory (extern/oics) add_subdirectory (extern/sdl4ogre) # Components add_subdirectory (components) +# Plugins +if (BUILD_MYGUI_PLUGIN) + add_subdirectory(plugins/mygui_resource_plugin) +endif() + +#Testing +if (BUILD_NIFTEST) + add_subdirectory(components/nif/tests/) +endif(BUILD_NIFTEST) + # Apps and tools add_subdirectory( apps/openmw ) @@ -570,6 +615,16 @@ endif() if (WIN32) if (MSVC) + if (MULTITHREADED_BUILD) + set( MT_BUILD "/MP") + endif (MULTITHREADED_BUILD) + + foreach( OUTPUTCONFIG ${CMAKE_CONFIGURATION_TYPES} ) + string( TOUPPER ${OUTPUTCONFIG} OUTPUTCONFIG ) + set( CMAKE_RUNTIME_OUTPUT_DIRECTORY_${OUTPUTCONFIG} "$(SolutionDir)$(Configuration)" ) + set( CMAKE_LIBRARY_OUTPUT_DIRECTORY_${OUTPUTCONFIG} "$(ProjectDir)$(Configuration)" ) + endforeach( OUTPUTCONFIG ) + if (USE_DEBUG_CONSOLE) set_target_properties(openmw PROPERTIES LINK_FLAGS_DEBUG "/SUBSYSTEM:CONSOLE") set_target_properties(openmw PROPERTIES LINK_FLAGS_RELWITHDEBINFO "/SUBSYSTEM:CONSOLE") @@ -610,14 +665,19 @@ if (WIN32) 4251 # class 'XXXX' needs to have dll-interface to be used by clients of class 'YYYY' 4275 # non dll-interface struct 'XXXX' used as base for dll-interface class 'YYYY' + # caused by boost + 4191 # 'type cast' : unsafe conversion (1.56, thread_primitives.hpp, normally off) + # OpenMW specific warnings 4099 # Type mismatch, declared class or struct is defined with other type 4100 # Unreferenced formal parameter (-Wunused-parameter) 4127 # Conditional expression is constant 4242 # Storing value in a variable of a smaller type, possible loss of data 4244 # Storing value of one type in variable of another (size_t in int, for example) + 4267 # Conversion from 'size_t' to 'int', possible loss of data 4305 # Truncating value (double to float, for example) 4309 # Variable overflow, trying to store 128 in a signed char for example + 4351 # New behavior: elements of array 'array' will be default initialized (desired behavior) 4355 # Using 'this' in member initialization list 4505 # Unreferenced local function has been removed 4701 # Potentially uninitialized local variable used @@ -631,31 +691,35 @@ if (WIN32) # boost::wave has a few issues with signed / unsigned conversions, so we suppress those here set(SHINY_WARNINGS "${WARNINGS} /wd4245") - set_target_properties(shiny PROPERTIES COMPILE_FLAGS ${SHINY_WARNINGS}) + set_target_properties(shiny PROPERTIES COMPILE_FLAGS "${SHINY_WARNINGS} ${MT_BUILD}") # there's an unreferenced local variable in the ogre platform, suppress it set(SHINY_OGRE_WARNINGS "${WARNINGS} /wd4101") - set_target_properties(shiny.OgrePlatform PROPERTIES COMPILE_FLAGS ${SHINY_OGRE_WARNINGS}) - set_target_properties(sdl4ogre PROPERTIES COMPILE_FLAGS ${WARNINGS}) - set_target_properties(oics PROPERTIES COMPILE_FLAGS ${WARNINGS}) - set_target_properties(components PROPERTIES COMPILE_FLAGS ${WARNINGS}) + set_target_properties(shiny.OgrePlatform PROPERTIES COMPILE_FLAGS "${SHINY_OGRE_WARNINGS} ${MT_BUILD}") + set_target_properties(sdl4ogre PROPERTIES COMPILE_FLAGS "${WARNINGS} ${MT_BUILD}") + # oics uses tinyxml, which has an initialized but unused variable + set(OICS_WARNINGS "${WARNINGS} /wd4189") + set_target_properties(oics PROPERTIES COMPILE_FLAGS "${OICS_WARNINGS} ${MT_BUILD}") + set_target_properties(components PROPERTIES COMPILE_FLAGS "${WARNINGS} ${MT_BUILD}") if (BUILD_LAUNCHER) - set_target_properties(omwlauncher PROPERTIES COMPILE_FLAGS ${WARNINGS}) + set_target_properties(omwlauncher PROPERTIES COMPILE_FLAGS "${WARNINGS} ${MT_BUILD}") endif (BUILD_LAUNCHER) - set_target_properties(openmw PROPERTIES COMPILE_FLAGS ${WARNINGS}) + set_target_properties(openmw PROPERTIES COMPILE_FLAGS "${WARNINGS} ${MT_BUILD}") if (BUILD_BSATOOL) - set_target_properties(bsatool PROPERTIES COMPILE_FLAGS ${WARNINGS}) + set_target_properties(bsatool PROPERTIES COMPILE_FLAGS "${WARNINGS} ${MT_BUILD}") endif (BUILD_BSATOOL) if (BUILD_ESMTOOL) - set_target_properties(esmtool PROPERTIES COMPILE_FLAGS ${WARNINGS}) + set_target_properties(esmtool PROPERTIES COMPILE_FLAGS "${WARNINGS} ${MT_BUILD}") endif (BUILD_ESMTOOL) if (BUILD_WIZARD) set_target_properties(openmw-wizard PROPERTIES COMPILE_FLAGS ${WARNINGS}) endif (BUILD_WIZARD) if (BUILD_OPENCS) - set_target_properties(opencs PROPERTIES COMPILE_FLAGS ${WARNINGS}) + # QT triggers an informational warning that the object layout may differ when compiled with /vd2 + set(OPENCS_WARNINGS "${WARNINGS} ${MT_BUILD} /wd4435") + set_target_properties(opencs PROPERTIES COMPILE_FLAGS ${OPENCS_WARNINGS}) endif (BUILD_OPENCS) if (BUILD_MWINIIMPORTER) - set_target_properties(mwiniimport PROPERTIES COMPILE_FLAGS ${WARNINGS}) + set_target_properties(mwiniimport PROPERTIES COMPILE_FLAGS "${WARNINGS} ${MT_BUILD}") endif (BUILD_MWINIIMPORTER) endif(MSVC) @@ -789,3 +853,26 @@ if (APPLE) include(CPack) endif (APPLE) +# Doxygen Target -- simply run 'make doc' or 'make doc_pages' +# output directory for 'make doc' is "${OpenMW_BINARY_DIR}/docs/Doxygen" +# output directory for 'make doc_pages' is "${DOXYGEN_PAGES_OUTPUT_DIR}" if defined +# or "${OpenMW_BINARY_DIR}/docs/Pages" otherwise +find_package(Doxygen) +if (DOXYGEN_FOUND) + # determine output directory for doc_pages + if (NOT DEFINED DOXYGEN_PAGES_OUTPUT_DIR) + set(DOXYGEN_PAGES_OUTPUT_DIR "${OpenMW_BINARY_DIR}/docs/Pages") + endif () + configure_file(${OpenMW_SOURCE_DIR}/docs/Doxyfile.cmake ${OpenMW_BINARY_DIR}/docs/Doxyfile @ONLY) + configure_file(${OpenMW_SOURCE_DIR}/docs/DoxyfilePages.cmake ${OpenMW_BINARY_DIR}/docs/DoxyfilePages @ONLY) + add_custom_target(doc + ${DOXYGEN_EXECUTABLE} ${OpenMW_BINARY_DIR}/docs/Doxyfile + WORKING_DIRECTORY ${OpenMW_BINARY_DIR} + COMMENT "Generating Doxygen documentation at ${OpenMW_BINARY_DIR}/docs/Doxygen" + VERBATIM) + add_custom_target(doc_pages + ${DOXYGEN_EXECUTABLE} ${OpenMW_BINARY_DIR}/docs/DoxyfilePages + WORKING_DIRECTORY ${OpenMW_BINARY_DIR} + COMMENT "Generating documentation for the github-pages at ${DOXYGEN_PAGES_OUTPUT_DIR}" VERBATIM) +endif () + diff --git a/Docs/Doxyfile b/Docs/Doxyfile deleted file mode 100644 index 43c3312ad..000000000 --- a/Docs/Doxyfile +++ /dev/null @@ -1,1543 +0,0 @@ -# Doxyfile 1.5.8 - -# This file describes the settings to be used by the documentation system -# doxygen (www.doxygen.org) for a project -# -# All text after a hash (#) is considered a comment and will be ignored -# The format is: -# TAG = value [value, ...] -# For lists items can also be appended using: -# TAG += value [value, ...] -# Values that contain spaces should be placed between quotes (" ") - -#--------------------------------------------------------------------------- -# Project related configuration options -#--------------------------------------------------------------------------- - -# This tag specifies the encoding used for all characters in the config file -# that follow. The default is UTF-8 which is also the encoding used for all -# text before the first occurrence of this tag. Doxygen uses libiconv (or the -# iconv built into libc) for the transcoding. See -# http://www.gnu.org/software/libiconv for the list of possible encodings. - -DOXYFILE_ENCODING = UTF-8 - -# The PROJECT_NAME tag is a single word (or a sequence of words surrounded -# by quotes) that should identify the project. - -PROJECT_NAME = OpenMW - -# The PROJECT_NUMBER tag can be used to enter a project or revision number. -# This could be handy for archiving the generated documentation or -# if some version control system is used. - -PROJECT_NUMBER = - -# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) -# base path where the generated documentation will be put. -# If a relative path is entered, it will be relative to the location -# where doxygen was started. If left blank the current directory will be used. - -OUTPUT_DIRECTORY = Doxygen - -# If the CREATE_SUBDIRS tag is set to YES, then doxygen will create -# 4096 sub-directories (in 2 levels) under the output directory of each output -# format and will distribute the generated files over these directories. -# Enabling this option can be useful when feeding doxygen a huge amount of -# source files, where putting all generated files in the same directory would -# otherwise cause performance problems for the file system. - -CREATE_SUBDIRS = NO - -# The OUTPUT_LANGUAGE tag is used to specify the language in which all -# documentation generated by doxygen is written. Doxygen will use this -# information to generate all constant output in the proper language. -# The default language is English, other supported languages are: -# Afrikaans, Arabic, Brazilian, Catalan, Chinese, Chinese-Traditional, -# Croatian, Czech, Danish, Dutch, Farsi, Finnish, French, German, Greek, -# Hungarian, Italian, Japanese, Japanese-en (Japanese with English messages), -# Korean, Korean-en, Lithuanian, Norwegian, Macedonian, Persian, Polish, -# Portuguese, Romanian, Russian, Serbian, Serbian-Cyrilic, Slovak, Slovene, -# Spanish, Swedish, and Ukrainian. - -OUTPUT_LANGUAGE = English - -# If the BRIEF_MEMBER_DESC tag is set to YES (the default) Doxygen will -# include brief member descriptions after the members that are listed in -# the file and class documentation (similar to JavaDoc). -# Set to NO to disable this. - -BRIEF_MEMBER_DESC = YES - -# If the REPEAT_BRIEF tag is set to YES (the default) Doxygen will prepend -# the brief description of a member or function before the detailed description. -# Note: if both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the -# brief descriptions will be completely suppressed. - -REPEAT_BRIEF = YES - -# This tag implements a quasi-intelligent brief description abbreviator -# that is used to form the text in various listings. Each string -# in this list, if found as the leading text of the brief description, will be -# stripped from the text and the result after processing the whole list, is -# used as the annotated text. Otherwise, the brief description is used as-is. -# If left blank, the following values are used ("$name" is automatically -# replaced with the name of the entity): "The $name class" "The $name widget" -# "The $name file" "is" "provides" "specifies" "contains" -# "represents" "a" "an" "the" - -ABBREVIATE_BRIEF = "The $name class" \ - "The $name widget" \ - "The $name file" \ - is \ - provides \ - specifies \ - contains \ - represents \ - a \ - an \ - the - -# If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then -# Doxygen will generate a detailed section even if there is only a brief -# description. - -ALWAYS_DETAILED_SEC = NO - -# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all -# inherited members of a class in the documentation of that class as if those -# members were ordinary class members. Constructors, destructors and assignment -# operators of the base classes will not be shown. - -INLINE_INHERITED_MEMB = NO - -# If the FULL_PATH_NAMES tag is set to YES then Doxygen will prepend the full -# path before files name in the file list and in the header files. If set -# to NO the shortest path that makes the file name unique will be used. - -FULL_PATH_NAMES = YES - -# If the FULL_PATH_NAMES tag is set to YES then the STRIP_FROM_PATH tag -# can be used to strip a user-defined part of the path. Stripping is -# only done if one of the specified strings matches the left-hand part of -# the path. The tag can be used to show relative paths in the file list. -# If left blank the directory from which doxygen is run is used as the -# path to strip. - -STRIP_FROM_PATH = - -# The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of -# the path mentioned in the documentation of a class, which tells -# the reader which header file to include in order to use a class. -# If left blank only the name of the header file containing the class -# definition is used. Otherwise one should specify the include paths that -# are normally passed to the compiler using the -I flag. - -STRIP_FROM_INC_PATH = - -# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter -# (but less readable) file names. This can be useful is your file systems -# doesn't support long names like on DOS, Mac, or CD-ROM. - -SHORT_NAMES = NO - -# If the JAVADOC_AUTOBRIEF tag is set to YES then Doxygen -# will interpret the first line (until the first dot) of a JavaDoc-style -# comment as the brief description. If set to NO, the JavaDoc -# comments will behave just like regular Qt-style comments -# (thus requiring an explicit @brief command for a brief description.) - -JAVADOC_AUTOBRIEF = NO - -# If the QT_AUTOBRIEF tag is set to YES then Doxygen will -# interpret the first line (until the first dot) of a Qt-style -# comment as the brief description. If set to NO, the comments -# will behave just like regular Qt-style comments (thus requiring -# an explicit \brief command for a brief description.) - -QT_AUTOBRIEF = NO - -# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make Doxygen -# treat a multi-line C++ special comment block (i.e. a block of //! or /// -# comments) as a brief description. This used to be the default behaviour. -# The new default is to treat a multi-line C++ comment block as a detailed -# description. Set this tag to YES if you prefer the old behaviour instead. - -MULTILINE_CPP_IS_BRIEF = NO - -# If the INHERIT_DOCS tag is set to YES (the default) then an undocumented -# member inherits the documentation from any documented member that it -# re-implements. - -INHERIT_DOCS = YES - -# If the SEPARATE_MEMBER_PAGES tag is set to YES, then doxygen will produce -# a new page for each member. If set to NO, the documentation of a member will -# be part of the file/class/namespace that contains it. - -SEPARATE_MEMBER_PAGES = NO - -# The TAB_SIZE tag can be used to set the number of spaces in a tab. -# Doxygen uses this value to replace tabs by spaces in code fragments. - -TAB_SIZE = 8 - -# This tag can be used to specify a number of aliases that acts -# as commands in the documentation. An alias has the form "name=value". -# For example adding "sideeffect=\par Side Effects:\n" will allow you to -# put the command \sideeffect (or @sideeffect) in the documentation, which -# will result in a user-defined paragraph with heading "Side Effects:". -# You can put \n's in the value part of an alias to insert newlines. - -ALIASES = - -# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C -# sources only. Doxygen will then generate output that is more tailored for C. -# For instance, some of the names that are used will be different. The list -# of all members will be omitted, etc. - -OPTIMIZE_OUTPUT_FOR_C = NO - -# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java -# sources only. Doxygen will then generate output that is more tailored for -# Java. For instance, namespaces will be presented as packages, qualified -# scopes will look different, etc. - -OPTIMIZE_OUTPUT_JAVA = NO - -# Set the OPTIMIZE_FOR_FORTRAN tag to YES if your project consists of Fortran -# sources only. Doxygen will then generate output that is more tailored for -# Fortran. - -OPTIMIZE_FOR_FORTRAN = NO - -# Set the OPTIMIZE_OUTPUT_VHDL tag to YES if your project consists of VHDL -# sources. Doxygen will then generate output that is tailored for -# VHDL. - -OPTIMIZE_OUTPUT_VHDL = NO - -# Doxygen selects the parser to use depending on the extension of the files it parses. -# With this tag you can assign which parser to use for a given extension. -# Doxygen has a built-in mapping, but you can override or extend it using this tag. -# The format is ext=language, where ext is a file extension, and language is one of -# the parsers supported by doxygen: IDL, Java, Javascript, C#, C, C++, D, PHP, -# Objective-C, Python, Fortran, VHDL, C, C++. For instance to make doxygen treat -# .inc files as Fortran files (default is PHP), and .f files as C (default is Fortran), -# use: inc=Fortran f=C - -EXTENSION_MAPPING = - -# If you use STL classes (i.e. std::string, std::vector, etc.) but do not want -# to include (a tag file for) the STL sources as input, then you should -# set this tag to YES in order to let doxygen match functions declarations and -# definitions whose arguments contain STL classes (e.g. func(std::string); v.s. -# func(std::string) {}). This also make the inheritance and collaboration -# diagrams that involve STL classes more complete and accurate. - -BUILTIN_STL_SUPPORT = NO - -# If you use Microsoft's C++/CLI language, you should set this option to YES to -# enable parsing support. - -CPP_CLI_SUPPORT = NO - -# Set the SIP_SUPPORT tag to YES if your project consists of sip sources only. -# Doxygen will parse them like normal C++ but will assume all classes use public -# instead of private inheritance when no explicit protection keyword is present. - -SIP_SUPPORT = NO - -# For Microsoft's IDL there are propget and propput attributes to indicate getter -# and setter methods for a property. Setting this option to YES (the default) -# will make doxygen to replace the get and set methods by a property in the -# documentation. This will only work if the methods are indeed getting or -# setting a simple type. If this is not the case, or you want to show the -# methods anyway, you should set this option to NO. - -IDL_PROPERTY_SUPPORT = YES - -# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC -# tag is set to YES, then doxygen will reuse the documentation of the first -# member in the group (if any) for the other members of the group. By default -# all members of a group must be documented explicitly. - -DISTRIBUTE_GROUP_DOC = NO - -# Set the SUBGROUPING tag to YES (the default) to allow class member groups of -# the same type (for instance a group of public functions) to be put as a -# subgroup of that type (e.g. under the Public Functions section). Set it to -# NO to prevent subgrouping. Alternatively, this can be done per class using -# the \nosubgrouping command. - -SUBGROUPING = YES - -# When TYPEDEF_HIDES_STRUCT is enabled, a typedef of a struct, union, or enum -# is documented as struct, union, or enum with the name of the typedef. So -# typedef struct TypeS {} TypeT, will appear in the documentation as a struct -# with name TypeT. When disabled the typedef will appear as a member of a file, -# namespace, or class. And the struct will be named TypeS. This can typically -# be useful for C code in case the coding convention dictates that all compound -# types are typedef'ed and only the typedef is referenced, never the tag name. - -TYPEDEF_HIDES_STRUCT = NO - -# The SYMBOL_CACHE_SIZE determines the size of the internal cache use to -# determine which symbols to keep in memory and which to flush to disk. -# When the cache is full, less often used symbols will be written to disk. -# For small to medium size projects (<1000 input files) the default value is -# probably good enough. For larger projects a too small cache size can cause -# doxygen to be busy swapping symbols to and from disk most of the time -# causing a significant performance penality. -# If the system has enough physical memory increasing the cache will improve the -# performance by keeping more symbols in memory. Note that the value works on -# a logarithmic scale so increasing the size by one will rougly double the -# memory usage. The cache size is given by this formula: -# 2^(16+SYMBOL_CACHE_SIZE). The valid range is 0..9, the default is 0, -# corresponding to a cache size of 2^16 = 65536 symbols - -SYMBOL_CACHE_SIZE = 0 - -#--------------------------------------------------------------------------- -# Build related configuration options -#--------------------------------------------------------------------------- - -# If the EXTRACT_ALL tag is set to YES doxygen will assume all entities in -# documentation are documented, even if no documentation was available. -# Private class members and static file members will be hidden unless -# the EXTRACT_PRIVATE and EXTRACT_STATIC tags are set to YES - -EXTRACT_ALL = YES - -# If the EXTRACT_PRIVATE tag is set to YES all private members of a class -# will be included in the documentation. - -EXTRACT_PRIVATE = YES - -# If the EXTRACT_STATIC tag is set to YES all static members of a file -# will be included in the documentation. - -EXTRACT_STATIC = YES - -# If the EXTRACT_LOCAL_CLASSES tag is set to YES classes (and structs) -# defined locally in source files will be included in the documentation. -# If set to NO only classes defined in header files are included. - -EXTRACT_LOCAL_CLASSES = YES - -# This flag is only useful for Objective-C code. When set to YES local -# methods, which are defined in the implementation section but not in -# the interface are included in the documentation. -# If set to NO (the default) only methods in the interface are included. - -EXTRACT_LOCAL_METHODS = YES - -# If this flag is set to YES, the members of anonymous namespaces will be -# extracted and appear in the documentation as a namespace called -# 'anonymous_namespace{file}', where file will be replaced with the base -# name of the file that contains the anonymous namespace. By default -# anonymous namespace are hidden. - -EXTRACT_ANON_NSPACES = YES - -# If the HIDE_UNDOC_MEMBERS tag is set to YES, Doxygen will hide all -# undocumented members of documented classes, files or namespaces. -# If set to NO (the default) these members will be included in the -# various overviews, but no documentation section is generated. -# This option has no effect if EXTRACT_ALL is enabled. - -HIDE_UNDOC_MEMBERS = NO - -# If the HIDE_UNDOC_CLASSES tag is set to YES, Doxygen will hide all -# undocumented classes that are normally visible in the class hierarchy. -# If set to NO (the default) these classes will be included in the various -# overviews. This option has no effect if EXTRACT_ALL is enabled. - -HIDE_UNDOC_CLASSES = NO - -# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, Doxygen will hide all -# friend (class|struct|union) declarations. -# If set to NO (the default) these declarations will be included in the -# documentation. - -HIDE_FRIEND_COMPOUNDS = NO - -# If the HIDE_IN_BODY_DOCS tag is set to YES, Doxygen will hide any -# documentation blocks found inside the body of a function. -# If set to NO (the default) these blocks will be appended to the -# function's detailed documentation block. - -HIDE_IN_BODY_DOCS = NO - -# The INTERNAL_DOCS tag determines if documentation -# that is typed after a \internal command is included. If the tag is set -# to NO (the default) then the documentation will be excluded. -# Set it to YES to include the internal documentation. - -INTERNAL_DOCS = NO - -# If the CASE_SENSE_NAMES tag is set to NO then Doxygen will only generate -# file names in lower-case letters. If set to YES upper-case letters are also -# allowed. This is useful if you have classes or files whose names only differ -# in case and if your file system supports case sensitive file names. Windows -# and Mac users are advised to set this option to NO. - -CASE_SENSE_NAMES = YES - -# If the HIDE_SCOPE_NAMES tag is set to NO (the default) then Doxygen -# will show members with their full class and namespace scopes in the -# documentation. If set to YES the scope will be hidden. - -HIDE_SCOPE_NAMES = NO - -# If the SHOW_INCLUDE_FILES tag is set to YES (the default) then Doxygen -# will put a list of the files that are included by a file in the documentation -# of that file. - -SHOW_INCLUDE_FILES = YES - -# If the INLINE_INFO tag is set to YES (the default) then a tag [inline] -# is inserted in the documentation for inline members. - -INLINE_INFO = YES - -# If the SORT_MEMBER_DOCS tag is set to YES (the default) then doxygen -# will sort the (detailed) documentation of file and class members -# alphabetically by member name. If set to NO the members will appear in -# declaration order. - -SORT_MEMBER_DOCS = YES - -# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the -# brief documentation of file, namespace and class members alphabetically -# by member name. If set to NO (the default) the members will appear in -# declaration order. - -SORT_BRIEF_DOCS = NO - -# If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the -# hierarchy of group names into alphabetical order. If set to NO (the default) -# the group names will appear in their defined order. - -SORT_GROUP_NAMES = NO - -# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be -# sorted by fully-qualified names, including namespaces. If set to -# NO (the default), the class list will be sorted only by class name, -# not including the namespace part. -# Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES. -# Note: This option applies only to the class list, not to the -# alphabetical list. - -SORT_BY_SCOPE_NAME = NO - -# The GENERATE_TODOLIST tag can be used to enable (YES) or -# disable (NO) the todo list. This list is created by putting \todo -# commands in the documentation. - -GENERATE_TODOLIST = YES - -# The GENERATE_TESTLIST tag can be used to enable (YES) or -# disable (NO) the test list. This list is created by putting \test -# commands in the documentation. - -GENERATE_TESTLIST = YES - -# The GENERATE_BUGLIST tag can be used to enable (YES) or -# disable (NO) the bug list. This list is created by putting \bug -# commands in the documentation. - -GENERATE_BUGLIST = YES - -# The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or -# disable (NO) the deprecated list. This list is created by putting -# \deprecated commands in the documentation. - -GENERATE_DEPRECATEDLIST= YES - -# The ENABLED_SECTIONS tag can be used to enable conditional -# documentation sections, marked by \if sectionname ... \endif. - -ENABLED_SECTIONS = - -# The MAX_INITIALIZER_LINES tag determines the maximum number of lines -# the initial value of a variable or define consists of for it to appear in -# the documentation. If the initializer consists of more lines than specified -# here it will be hidden. Use a value of 0 to hide initializers completely. -# The appearance of the initializer of individual variables and defines in the -# documentation can be controlled using \showinitializer or \hideinitializer -# command in the documentation regardless of this setting. - -MAX_INITIALIZER_LINES = 30 - -# Set the SHOW_USED_FILES tag to NO to disable the list of files generated -# at the bottom of the documentation of classes and structs. If set to YES the -# list will mention the files that were used to generate the documentation. - -SHOW_USED_FILES = YES - -# If the sources in your project are distributed over multiple directories -# then setting the SHOW_DIRECTORIES tag to YES will show the directory hierarchy -# in the documentation. The default is NO. - -SHOW_DIRECTORIES = NO - -# Set the SHOW_FILES tag to NO to disable the generation of the Files page. -# This will remove the Files entry from the Quick Index and from the -# Folder Tree View (if specified). The default is YES. - -SHOW_FILES = YES - -# Set the SHOW_NAMESPACES tag to NO to disable the generation of the -# Namespaces page. This will remove the Namespaces entry from the Quick Index -# and from the Folder Tree View (if specified). The default is YES. - -SHOW_NAMESPACES = YES - -# The FILE_VERSION_FILTER tag can be used to specify a program or script that -# doxygen should invoke to get the current version for each file (typically from -# the version control system). Doxygen will invoke the program by executing (via -# popen()) the command , where is the value of -# the FILE_VERSION_FILTER tag, and is the name of an input file -# provided by doxygen. Whatever the program writes to standard output -# is used as the file version. See the manual for examples. - -FILE_VERSION_FILTER = - -# The LAYOUT_FILE tag can be used to specify a layout file which will be parsed by -# doxygen. The layout file controls the global structure of the generated output files -# in an output format independent way. The create the layout file that represents -# doxygen's defaults, run doxygen with the -l option. You can optionally specify a -# file name after the option, if omitted DoxygenLayout.xml will be used as the name -# of the layout file. - -LAYOUT_FILE = - -#--------------------------------------------------------------------------- -# configuration options related to warning and progress messages -#--------------------------------------------------------------------------- - -# The QUIET tag can be used to turn on/off the messages that are generated -# by doxygen. Possible values are YES and NO. If left blank NO is used. - -QUIET = NO - -# The WARNINGS tag can be used to turn on/off the warning messages that are -# generated by doxygen. Possible values are YES and NO. If left blank -# NO is used. - -WARNINGS = YES - -# If WARN_IF_UNDOCUMENTED is set to YES, then doxygen will generate warnings -# for undocumented members. If EXTRACT_ALL is set to YES then this flag will -# automatically be disabled. - -WARN_IF_UNDOCUMENTED = NO - -# If WARN_IF_DOC_ERROR is set to YES, doxygen will generate warnings for -# potential errors in the documentation, such as not documenting some -# parameters in a documented function, or documenting parameters that -# don't exist or using markup commands wrongly. - -WARN_IF_DOC_ERROR = YES - -# This WARN_NO_PARAMDOC option can be abled to get warnings for -# functions that are documented, but have no documentation for their parameters -# or return value. If set to NO (the default) doxygen will only warn about -# wrong or incomplete parameter documentation, but not about the absence of -# documentation. - -WARN_NO_PARAMDOC = NO - -# The WARN_FORMAT tag determines the format of the warning messages that -# doxygen can produce. The string should contain the $file, $line, and $text -# tags, which will be replaced by the file and line number from which the -# warning originated and the warning text. Optionally the format may contain -# $version, which will be replaced by the version of the file (if it could -# be obtained via FILE_VERSION_FILTER) - -WARN_FORMAT = "$file:$line: $text" - -# The WARN_LOGFILE tag can be used to specify a file to which warning -# and error messages should be written. If left blank the output is written -# to stderr. - -WARN_LOGFILE = - -#--------------------------------------------------------------------------- -# configuration options related to the input files -#--------------------------------------------------------------------------- - -# The INPUT tag can be used to specify the files and/or directories that contain -# documented source files. You may enter file names like "myfile.cpp" or -# directories like "/usr/src/myproject". Separate the files or directories -# with spaces. - -INPUT = apps \ - components \ - libs \ - Docs - -# This tag can be used to specify the character encoding of the source files -# that doxygen parses. Internally doxygen uses the UTF-8 encoding, which is -# also the default input encoding. Doxygen uses libiconv (or the iconv built -# into libc) for the transcoding. See http://www.gnu.org/software/libiconv for -# the list of possible encodings. - -INPUT_ENCODING = UTF-8 - -# If the value of the INPUT tag contains directories, you can use the -# FILE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp -# and *.h) to filter out the source-files in the directories. If left -# blank the following patterns are tested: -# *.c *.cc *.cxx *.cpp *.c++ *.java *.ii *.ixx *.ipp *.i++ *.inl *.h *.hh *.hxx -# *.hpp *.h++ *.idl *.odl *.cs *.php *.php3 *.inc *.m *.mm *.py *.f90 - -FILE_PATTERNS = *.c \ - *.cc \ - *.cxx \ - *.cpp \ - *.c++ \ - *.java \ - *.ii \ - *.ixx \ - *.ipp \ - *.i++ \ - *.inl \ - *.h \ - *.hh \ - *.hxx \ - *.hpp \ - *.h++ \ - *.idl \ - *.odl \ - *.cs \ - *.php \ - *.php3 \ - *.inc \ - *.m \ - *.mm \ - *.dox \ - *.py \ - *.f90 \ - *.f \ - *.vhd \ - *.vhdl - -# The RECURSIVE tag can be used to turn specify whether or not subdirectories -# should be searched for input files as well. Possible values are YES and NO. -# If left blank NO is used. - -RECURSIVE = YES - -# The EXCLUDE tag can be used to specify files and/or directories that should -# excluded from the INPUT source files. This way you can easily exclude a -# subdirectory from a directory tree whose root is specified with the INPUT tag. - -EXCLUDE = - -# The EXCLUDE_SYMLINKS tag can be used select whether or not files or -# directories that are symbolic links (a Unix filesystem feature) are excluded -# from the input. - -EXCLUDE_SYMLINKS = NO - -# If the value of the INPUT tag contains directories, you can use the -# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude -# certain files from those directories. Note that the wildcards are matched -# against the file with absolute path, so to exclude all test directories -# for example use the pattern */test/* - -EXCLUDE_PATTERNS = - -# The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names -# (namespaces, classes, functions, etc.) that should be excluded from the -# output. The symbol name can be a fully qualified name, a word, or if the -# wildcard * is used, a substring. Examples: ANamespace, AClass, -# AClass::ANamespace, ANamespace::*Test - -EXCLUDE_SYMBOLS = - -# The EXAMPLE_PATH tag can be used to specify one or more files or -# directories that contain example code fragments that are included (see -# the \include command). - -EXAMPLE_PATH = - -# If the value of the EXAMPLE_PATH tag contains directories, you can use the -# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp -# and *.h) to filter out the source-files in the directories. If left -# blank all files are included. - -EXAMPLE_PATTERNS = * - -# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be -# searched for input files to be used with the \include or \dontinclude -# commands irrespective of the value of the RECURSIVE tag. -# Possible values are YES and NO. If left blank NO is used. - -EXAMPLE_RECURSIVE = NO - -# The IMAGE_PATH tag can be used to specify one or more files or -# directories that contain image that are included in the documentation (see -# the \image command). - -IMAGE_PATH = - -# The INPUT_FILTER tag can be used to specify a program that doxygen should -# invoke to filter for each input file. Doxygen will invoke the filter program -# by executing (via popen()) the command , where -# is the value of the INPUT_FILTER tag, and is the name of an -# input file. Doxygen will then use the output that the filter program writes -# to standard output. If FILTER_PATTERNS is specified, this tag will be -# ignored. - -INPUT_FILTER = - -# The FILTER_PATTERNS tag can be used to specify filters on a per file pattern -# basis. Doxygen will compare the file name with each pattern and apply the -# filter if there is a match. The filters are a list of the form: -# pattern=filter (like *.cpp=my_cpp_filter). See INPUT_FILTER for further -# info on how filters are used. If FILTER_PATTERNS is empty, INPUT_FILTER -# is applied to all files. - -FILTER_PATTERNS = - -# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using -# INPUT_FILTER) will be used to filter the input files when producing source -# files to browse (i.e. when SOURCE_BROWSER is set to YES). - -FILTER_SOURCE_FILES = NO - -#--------------------------------------------------------------------------- -# configuration options related to source browsing -#--------------------------------------------------------------------------- - -# If the SOURCE_BROWSER tag is set to YES then a list of source files will -# be generated. Documented entities will be cross-referenced with these sources. -# Note: To get rid of all source code in the generated output, make sure also -# VERBATIM_HEADERS is set to NO. - -SOURCE_BROWSER = NO - -# Setting the INLINE_SOURCES tag to YES will include the body -# of functions and classes directly in the documentation. - -INLINE_SOURCES = NO - -# Setting the STRIP_CODE_COMMENTS tag to YES (the default) will instruct -# doxygen to hide any special comment blocks from generated source code -# fragments. Normal C and C++ comments will always remain visible. - -STRIP_CODE_COMMENTS = YES - -# If the REFERENCED_BY_RELATION tag is set to YES -# then for each documented function all documented -# functions referencing it will be listed. - -REFERENCED_BY_RELATION = NO - -# If the REFERENCES_RELATION tag is set to YES -# then for each documented function all documented entities -# called/used by that function will be listed. - -REFERENCES_RELATION = NO - -# If the REFERENCES_LINK_SOURCE tag is set to YES (the default) -# and SOURCE_BROWSER tag is set to YES, then the hyperlinks from -# functions in REFERENCES_RELATION and REFERENCED_BY_RELATION lists will -# link to the source code. Otherwise they will link to the documentation. - -REFERENCES_LINK_SOURCE = YES - -# If the USE_HTAGS tag is set to YES then the references to source code -# will point to the HTML generated by the htags(1) tool instead of doxygen -# built-in source browser. The htags tool is part of GNU's global source -# tagging system (see http://www.gnu.org/software/global/global.html). You -# will need version 4.8.6 or higher. - -USE_HTAGS = NO - -# If the VERBATIM_HEADERS tag is set to YES (the default) then Doxygen -# will generate a verbatim copy of the header file for each class for -# which an include is specified. Set to NO to disable this. - -VERBATIM_HEADERS = YES - -#--------------------------------------------------------------------------- -# configuration options related to the alphabetical class index -#--------------------------------------------------------------------------- - -# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index -# of all compounds will be generated. Enable this if the project -# contains a lot of classes, structs, unions or interfaces. - -ALPHABETICAL_INDEX = NO - -# If the alphabetical index is enabled (see ALPHABETICAL_INDEX) then -# the COLS_IN_ALPHA_INDEX tag can be used to specify the number of columns -# in which this list will be split (can be a number in the range [1..20]) - -COLS_IN_ALPHA_INDEX = 5 - -# In case all classes in a project start with a common prefix, all -# classes will be put under the same header in the alphabetical index. -# The IGNORE_PREFIX tag can be used to specify one or more prefixes that -# should be ignored while generating the index headers. - -IGNORE_PREFIX = - -#--------------------------------------------------------------------------- -# configuration options related to the HTML output -#--------------------------------------------------------------------------- - -# If the GENERATE_HTML tag is set to YES (the default) Doxygen will -# generate HTML output. - -GENERATE_HTML = YES - -# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. -# If a relative path is entered the value of OUTPUT_DIRECTORY will be -# put in front of it. If left blank `html' will be used as the default path. - -HTML_OUTPUT = html - -# The HTML_FILE_EXTENSION tag can be used to specify the file extension for -# each generated HTML page (for example: .htm,.php,.asp). If it is left blank -# doxygen will generate files with .html extension. - -HTML_FILE_EXTENSION = .html - -# The HTML_HEADER tag can be used to specify a personal HTML header for -# each generated HTML page. If it is left blank doxygen will generate a -# standard header. - -HTML_HEADER = - -# The HTML_FOOTER tag can be used to specify a personal HTML footer for -# each generated HTML page. If it is left blank doxygen will generate a -# standard footer. - -HTML_FOOTER = - -# The HTML_STYLESHEET tag can be used to specify a user-defined cascading -# style sheet that is used by each HTML page. It can be used to -# fine-tune the look of the HTML output. If the tag is left blank doxygen -# will generate a default style sheet. Note that doxygen will try to copy -# the style sheet file to the HTML output directory, so don't put your own -# stylesheet in the HTML output directory as well, or it will be erased! - -HTML_STYLESHEET = - -# If the HTML_ALIGN_MEMBERS tag is set to YES, the members of classes, -# files or namespaces will be aligned in HTML using tables. If set to -# NO a bullet list will be used. - -HTML_ALIGN_MEMBERS = YES - -# If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML -# documentation will contain sections that can be hidden and shown after the -# page has loaded. For this to work a browser that supports -# JavaScript and DHTML is required (for instance Mozilla 1.0+, Firefox -# Netscape 6.0+, Internet explorer 5.0+, Konqueror, or Safari). - -HTML_DYNAMIC_SECTIONS = NO - -# If the GENERATE_DOCSET tag is set to YES, additional index files -# will be generated that can be used as input for Apple's Xcode 3 -# integrated development environment, introduced with OSX 10.5 (Leopard). -# To create a documentation set, doxygen will generate a Makefile in the -# HTML output directory. Running make will produce the docset in that -# directory and running "make install" will install the docset in -# ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find -# it at startup. -# See http://developer.apple.com/tools/creatingdocsetswithdoxygen.html for more information. - -GENERATE_DOCSET = NO - -# When GENERATE_DOCSET tag is set to YES, this tag determines the name of the -# feed. A documentation feed provides an umbrella under which multiple -# documentation sets from a single provider (such as a company or product suite) -# can be grouped. - -DOCSET_FEEDNAME = "Doxygen generated docs" - -# When GENERATE_DOCSET tag is set to YES, this tag specifies a string that -# should uniquely identify the documentation set bundle. This should be a -# reverse domain-name style string, e.g. com.mycompany.MyDocSet. Doxygen -# will append .docset to the name. - -DOCSET_BUNDLE_ID = org.doxygen.Project - -# If the GENERATE_HTMLHELP tag is set to YES, additional index files -# will be generated that can be used as input for tools like the -# Microsoft HTML help workshop to generate a compiled HTML help file (.chm) -# of the generated HTML documentation. - -GENERATE_HTMLHELP = NO - -# If the GENERATE_HTMLHELP tag is set to YES, the CHM_FILE tag can -# be used to specify the file name of the resulting .chm file. You -# can add a path in front of the file if the result should not be -# written to the html output directory. - -CHM_FILE = - -# If the GENERATE_HTMLHELP tag is set to YES, the HHC_LOCATION tag can -# be used to specify the location (absolute path including file name) of -# the HTML help compiler (hhc.exe). If non-empty doxygen will try to run -# the HTML help compiler on the generated index.hhp. - -HHC_LOCATION = - -# If the GENERATE_HTMLHELP tag is set to YES, the GENERATE_CHI flag -# controls if a separate .chi index file is generated (YES) or that -# it should be included in the master .chm file (NO). - -GENERATE_CHI = NO - -# If the GENERATE_HTMLHELP tag is set to YES, the CHM_INDEX_ENCODING -# is used to encode HtmlHelp index (hhk), content (hhc) and project file -# content. - -CHM_INDEX_ENCODING = - -# If the GENERATE_HTMLHELP tag is set to YES, the BINARY_TOC flag -# controls whether a binary table of contents is generated (YES) or a -# normal table of contents (NO) in the .chm file. - -BINARY_TOC = NO - -# The TOC_EXPAND flag can be set to YES to add extra items for group members -# to the contents of the HTML help documentation and to the tree view. - -TOC_EXPAND = NO - -# If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and QHP_VIRTUAL_FOLDER -# are set, an additional index file will be generated that can be used as input for -# Qt's qhelpgenerator to generate a Qt Compressed Help (.qch) of the generated -# HTML documentation. - -GENERATE_QHP = NO - -# If the QHG_LOCATION tag is specified, the QCH_FILE tag can -# be used to specify the file name of the resulting .qch file. -# The path specified is relative to the HTML output folder. - -QCH_FILE = - -# The QHP_NAMESPACE tag specifies the namespace to use when generating -# Qt Help Project output. For more information please see -# http://doc.trolltech.com/qthelpproject.html#namespace - -QHP_NAMESPACE = - -# The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating -# Qt Help Project output. For more information please see -# http://doc.trolltech.com/qthelpproject.html#virtual-folders - -QHP_VIRTUAL_FOLDER = doc - -# If QHP_CUST_FILTER_NAME is set, it specifies the name of a custom filter to add. -# For more information please see -# http://doc.trolltech.com/qthelpproject.html#custom-filters - -QHP_CUST_FILTER_NAME = - -# The QHP_CUST_FILT_ATTRS tag specifies the list of the attributes of the custom filter to add.For more information please see -# Qt Help Project / Custom Filters. - -QHP_CUST_FILTER_ATTRS = - -# The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this project's -# filter section matches. -# Qt Help Project / Filter Attributes. - -QHP_SECT_FILTER_ATTRS = - -# If the GENERATE_QHP tag is set to YES, the QHG_LOCATION tag can -# be used to specify the location of Qt's qhelpgenerator. -# If non-empty doxygen will try to run qhelpgenerator on the generated -# .qhp file. - -QHG_LOCATION = - -# The DISABLE_INDEX tag can be used to turn on/off the condensed index at -# top of each HTML page. The value NO (the default) enables the index and -# the value YES disables it. - -DISABLE_INDEX = NO - -# This tag can be used to set the number of enum values (range [1..20]) -# that doxygen will group on one line in the generated HTML documentation. - -ENUM_VALUES_PER_LINE = 4 - -# The GENERATE_TREEVIEW tag is used to specify whether a tree-like index -# structure should be generated to display hierarchical information. -# If the tag value is set to FRAME, a side panel will be generated -# containing a tree-like index structure (just like the one that -# is generated for HTML Help). For this to work a browser that supports -# JavaScript, DHTML, CSS and frames is required (for instance Mozilla 1.0+, -# Netscape 6.0+, Internet explorer 5.0+, or Konqueror). Windows users are -# probably better off using the HTML help feature. Other possible values -# for this tag are: HIERARCHIES, which will generate the Groups, Directories, -# and Class Hierarchy pages using a tree view instead of an ordered list; -# ALL, which combines the behavior of FRAME and HIERARCHIES; and NONE, which -# disables this behavior completely. For backwards compatibility with previous -# releases of Doxygen, the values YES and NO are equivalent to FRAME and NONE -# respectively. - -GENERATE_TREEVIEW = NONE - -# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be -# used to set the initial width (in pixels) of the frame in which the tree -# is shown. - -TREEVIEW_WIDTH = 250 - -# Use this tag to change the font size of Latex formulas included -# as images in the HTML documentation. The default is 10. Note that -# when you change the font size after a successful doxygen run you need -# to manually remove any form_*.png images from the HTML output directory -# to force them to be regenerated. - -FORMULA_FONTSIZE = 10 - -#--------------------------------------------------------------------------- -# configuration options related to the LaTeX output -#--------------------------------------------------------------------------- - -# If the GENERATE_LATEX tag is set to YES (the default) Doxygen will -# generate Latex output. - -GENERATE_LATEX = NO - -# The LATEX_OUTPUT tag is used to specify where the LaTeX docs will be put. -# If a relative path is entered the value of OUTPUT_DIRECTORY will be -# put in front of it. If left blank `latex' will be used as the default path. - -LATEX_OUTPUT = latex - -# The LATEX_CMD_NAME tag can be used to specify the LaTeX command name to be -# invoked. If left blank `latex' will be used as the default command name. - -LATEX_CMD_NAME = latex - -# The MAKEINDEX_CMD_NAME tag can be used to specify the command name to -# generate index for LaTeX. If left blank `makeindex' will be used as the -# default command name. - -MAKEINDEX_CMD_NAME = makeindex - -# If the COMPACT_LATEX tag is set to YES Doxygen generates more compact -# LaTeX documents. This may be useful for small projects and may help to -# save some trees in general. - -COMPACT_LATEX = NO - -# The PAPER_TYPE tag can be used to set the paper type that is used -# by the printer. Possible values are: a4, a4wide, letter, legal and -# executive. If left blank a4wide will be used. - -PAPER_TYPE = a4wide - -# The EXTRA_PACKAGES tag can be to specify one or more names of LaTeX -# packages that should be included in the LaTeX output. - -EXTRA_PACKAGES = - -# The LATEX_HEADER tag can be used to specify a personal LaTeX header for -# the generated latex document. The header should contain everything until -# the first chapter. If it is left blank doxygen will generate a -# standard header. Notice: only use this tag if you know what you are doing! - -LATEX_HEADER = - -# If the PDF_HYPERLINKS tag is set to YES, the LaTeX that is generated -# is prepared for conversion to pdf (using ps2pdf). The pdf file will -# contain links (just like the HTML output) instead of page references -# This makes the output suitable for online browsing using a pdf viewer. - -PDF_HYPERLINKS = YES - -# If the USE_PDFLATEX tag is set to YES, pdflatex will be used instead of -# plain latex in the generated Makefile. Set this option to YES to get a -# higher quality PDF documentation. - -USE_PDFLATEX = YES - -# If the LATEX_BATCHMODE tag is set to YES, doxygen will add the \\batchmode. -# command to the generated LaTeX files. This will instruct LaTeX to keep -# running if errors occur, instead of asking the user for help. -# This option is also used when generating formulas in HTML. - -LATEX_BATCHMODE = NO - -# If LATEX_HIDE_INDICES is set to YES then doxygen will not -# include the index chapters (such as File Index, Compound Index, etc.) -# in the output. - -LATEX_HIDE_INDICES = NO - -#--------------------------------------------------------------------------- -# configuration options related to the RTF output -#--------------------------------------------------------------------------- - -# If the GENERATE_RTF tag is set to YES Doxygen will generate RTF output -# The RTF output is optimized for Word 97 and may not look very pretty with -# other RTF readers or editors. - -GENERATE_RTF = NO - -# The RTF_OUTPUT tag is used to specify where the RTF docs will be put. -# If a relative path is entered the value of OUTPUT_DIRECTORY will be -# put in front of it. If left blank `rtf' will be used as the default path. - -RTF_OUTPUT = rtf - -# If the COMPACT_RTF tag is set to YES Doxygen generates more compact -# RTF documents. This may be useful for small projects and may help to -# save some trees in general. - -COMPACT_RTF = NO - -# If the RTF_HYPERLINKS tag is set to YES, the RTF that is generated -# will contain hyperlink fields. The RTF file will -# contain links (just like the HTML output) instead of page references. -# This makes the output suitable for online browsing using WORD or other -# programs which support those fields. -# Note: wordpad (write) and others do not support links. - -RTF_HYPERLINKS = NO - -# Load stylesheet definitions from file. Syntax is similar to doxygen's -# config file, i.e. a series of assignments. You only have to provide -# replacements, missing definitions are set to their default value. - -RTF_STYLESHEET_FILE = - -# Set optional variables used in the generation of an rtf document. -# Syntax is similar to doxygen's config file. - -RTF_EXTENSIONS_FILE = - -#--------------------------------------------------------------------------- -# configuration options related to the man page output -#--------------------------------------------------------------------------- - -# If the GENERATE_MAN tag is set to YES (the default) Doxygen will -# generate man pages - -GENERATE_MAN = NO - -# The MAN_OUTPUT tag is used to specify where the man pages will be put. -# If a relative path is entered the value of OUTPUT_DIRECTORY will be -# put in front of it. If left blank `man' will be used as the default path. - -MAN_OUTPUT = man - -# The MAN_EXTENSION tag determines the extension that is added to -# the generated man pages (default is the subroutine's section .3) - -MAN_EXTENSION = .3 - -# If the MAN_LINKS tag is set to YES and Doxygen generates man output, -# then it will generate one additional man file for each entity -# documented in the real man page(s). These additional files -# only source the real man page, but without them the man command -# would be unable to find the correct page. The default is NO. - -MAN_LINKS = NO - -#--------------------------------------------------------------------------- -# configuration options related to the XML output -#--------------------------------------------------------------------------- - -# If the GENERATE_XML tag is set to YES Doxygen will -# generate an XML file that captures the structure of -# the code including all documentation. - -GENERATE_XML = NO - -# The XML_OUTPUT tag is used to specify where the XML pages will be put. -# If a relative path is entered the value of OUTPUT_DIRECTORY will be -# put in front of it. If left blank `xml' will be used as the default path. - -XML_OUTPUT = xml - -# The XML_SCHEMA tag can be used to specify an XML schema, -# which can be used by a validating XML parser to check the -# syntax of the XML files. - -XML_SCHEMA = - -# The XML_DTD tag can be used to specify an XML DTD, -# which can be used by a validating XML parser to check the -# syntax of the XML files. - -XML_DTD = - -# If the XML_PROGRAMLISTING tag is set to YES Doxygen will -# dump the program listings (including syntax highlighting -# and cross-referencing information) to the XML output. Note that -# enabling this will significantly increase the size of the XML output. - -XML_PROGRAMLISTING = YES - -#--------------------------------------------------------------------------- -# configuration options for the AutoGen Definitions output -#--------------------------------------------------------------------------- - -# If the GENERATE_AUTOGEN_DEF tag is set to YES Doxygen will -# generate an AutoGen Definitions (see autogen.sf.net) file -# that captures the structure of the code including all -# documentation. Note that this feature is still experimental -# and incomplete at the moment. - -GENERATE_AUTOGEN_DEF = NO - -#--------------------------------------------------------------------------- -# configuration options related to the Perl module output -#--------------------------------------------------------------------------- - -# If the GENERATE_PERLMOD tag is set to YES Doxygen will -# generate a Perl module file that captures the structure of -# the code including all documentation. Note that this -# feature is still experimental and incomplete at the -# moment. - -GENERATE_PERLMOD = NO - -# If the PERLMOD_LATEX tag is set to YES Doxygen will generate -# the necessary Makefile rules, Perl scripts and LaTeX code to be able -# to generate PDF and DVI output from the Perl module output. - -PERLMOD_LATEX = NO - -# If the PERLMOD_PRETTY tag is set to YES the Perl module output will be -# nicely formatted so it can be parsed by a human reader. This is useful -# if you want to understand what is going on. On the other hand, if this -# tag is set to NO the size of the Perl module output will be much smaller -# and Perl will parse it just the same. - -PERLMOD_PRETTY = YES - -# The names of the make variables in the generated doxyrules.make file -# are prefixed with the string contained in PERLMOD_MAKEVAR_PREFIX. -# This is useful so different doxyrules.make files included by the same -# Makefile don't overwrite each other's variables. - -PERLMOD_MAKEVAR_PREFIX = - -#--------------------------------------------------------------------------- -# Configuration options related to the preprocessor -#--------------------------------------------------------------------------- - -# If the ENABLE_PREPROCESSING tag is set to YES (the default) Doxygen will -# evaluate all C-preprocessor directives found in the sources and include -# files. - -ENABLE_PREPROCESSING = YES - -# If the MACRO_EXPANSION tag is set to YES Doxygen will expand all macro -# names in the source code. If set to NO (the default) only conditional -# compilation will be performed. Macro expansion can be done in a controlled -# way by setting EXPAND_ONLY_PREDEF to YES. - -MACRO_EXPANSION = NO - -# If the EXPAND_ONLY_PREDEF and MACRO_EXPANSION tags are both set to YES -# then the macro expansion is limited to the macros specified with the -# PREDEFINED and EXPAND_AS_DEFINED tags. - -EXPAND_ONLY_PREDEF = NO - -# If the SEARCH_INCLUDES tag is set to YES (the default) the includes files -# in the INCLUDE_PATH (see below) will be search if a #include is found. - -SEARCH_INCLUDES = YES - -# The INCLUDE_PATH tag can be used to specify one or more directories that -# contain include files that are not input files but should be processed by -# the preprocessor. - -INCLUDE_PATH = - -# You can use the INCLUDE_FILE_PATTERNS tag to specify one or more wildcard -# patterns (like *.h and *.hpp) to filter out the header-files in the -# directories. If left blank, the patterns specified with FILE_PATTERNS will -# be used. - -INCLUDE_FILE_PATTERNS = - -# The PREDEFINED tag can be used to specify one or more macro names that -# are defined before the preprocessor is started (similar to the -D option of -# gcc). The argument of the tag is a list of macros of the form: name -# or name=definition (no spaces). If the definition and the = are -# omitted =1 is assumed. To prevent a macro definition from being -# undefined via #undef or recursively expanded use the := operator -# instead of the = operator. - -PREDEFINED = - -# If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then -# this tag can be used to specify a list of macro names that should be expanded. -# The macro definition that is found in the sources will be used. -# Use the PREDEFINED tag if you want to use a different macro definition. - -EXPAND_AS_DEFINED = - -# If the SKIP_FUNCTION_MACROS tag is set to YES (the default) then -# doxygen's preprocessor will remove all function-like macros that are alone -# on a line, have an all uppercase name, and do not end with a semicolon. Such -# function macros are typically used for boiler-plate code, and will confuse -# the parser if not removed. - -SKIP_FUNCTION_MACROS = YES - -#--------------------------------------------------------------------------- -# Configuration::additions related to external references -#--------------------------------------------------------------------------- - -# The TAGFILES option can be used to specify one or more tagfiles. -# Optionally an initial location of the external documentation -# can be added for each tagfile. The format of a tag file without -# this location is as follows: -# TAGFILES = file1 file2 ... -# Adding location for the tag files is done as follows: -# TAGFILES = file1=loc1 "file2 = loc2" ... -# where "loc1" and "loc2" can be relative or absolute paths or -# URLs. If a location is present for each tag, the installdox tool -# does not have to be run to correct the links. -# Note that each tag file must have a unique name -# (where the name does NOT include the path) -# If a tag file is not located in the directory in which doxygen -# is run, you must also specify the path to the tagfile here. - -TAGFILES = - -# When a file name is specified after GENERATE_TAGFILE, doxygen will create -# a tag file that is based on the input files it reads. - -GENERATE_TAGFILE = - -# If the ALLEXTERNALS tag is set to YES all external classes will be listed -# in the class index. If set to NO only the inherited external classes -# will be listed. - -ALLEXTERNALS = NO - -# If the EXTERNAL_GROUPS tag is set to YES all external groups will be listed -# in the modules index. If set to NO, only the current project's groups will -# be listed. - -EXTERNAL_GROUPS = YES - -# The PERL_PATH should be the absolute path and name of the perl script -# interpreter (i.e. the result of `which perl'). - -PERL_PATH = /usr/bin/perl - -#--------------------------------------------------------------------------- -# Configuration options related to the dot tool -#--------------------------------------------------------------------------- - -# If the CLASS_DIAGRAMS tag is set to YES (the default) Doxygen will -# generate a inheritance diagram (in HTML, RTF and LaTeX) for classes with base -# or super classes. Setting the tag to NO turns the diagrams off. Note that -# this option is superseded by the HAVE_DOT option below. This is only a -# fallback. It is recommended to install and use dot, since it yields more -# powerful graphs. - -CLASS_DIAGRAMS = YES - -# You can define message sequence charts within doxygen comments using the \msc -# command. Doxygen will then run the mscgen tool (see -# http://www.mcternan.me.uk/mscgen/) to produce the chart and insert it in the -# documentation. The MSCGEN_PATH tag allows you to specify the directory where -# the mscgen tool resides. If left empty the tool is assumed to be found in the -# default search path. - -MSCGEN_PATH = - -# If set to YES, the inheritance and collaboration graphs will hide -# inheritance and usage relations if the target is undocumented -# or is not a class. - -HIDE_UNDOC_RELATIONS = YES - -# If you set the HAVE_DOT tag to YES then doxygen will assume the dot tool is -# available from the path. This tool is part of Graphviz, a graph visualization -# toolkit from AT&T and Lucent Bell Labs. The other options in this section -# have no effect if this option is set to NO (the default) - -HAVE_DOT = YES - -# By default doxygen will write a font called FreeSans.ttf to the output -# directory and reference it in all dot files that doxygen generates. This -# font does not include all possible unicode characters however, so when you need -# these (or just want a differently looking font) you can specify the font name -# using DOT_FONTNAME. You need need to make sure dot is able to find the font, -# which can be done by putting it in a standard location or by setting the -# DOTFONTPATH environment variable or by setting DOT_FONTPATH to the directory -# containing the font. - -DOT_FONTNAME = FreeSans - -# The DOT_FONTSIZE tag can be used to set the size of the font of dot graphs. -# The default size is 10pt. - -DOT_FONTSIZE = 10 - -# By default doxygen will tell dot to use the output directory to look for the -# FreeSans.ttf font (which doxygen will put there itself). If you specify a -# different font using DOT_FONTNAME you can set the path where dot -# can find it using this tag. - -DOT_FONTPATH = - -# If the CLASS_GRAPH and HAVE_DOT tags are set to YES then doxygen -# will generate a graph for each documented class showing the direct and -# indirect inheritance relations. Setting this tag to YES will force the -# the CLASS_DIAGRAMS tag to NO. - -CLASS_GRAPH = YES - -# If the COLLABORATION_GRAPH and HAVE_DOT tags are set to YES then doxygen -# will generate a graph for each documented class showing the direct and -# indirect implementation dependencies (inheritance, containment, and -# class references variables) of the class with other documented classes. - -COLLABORATION_GRAPH = YES - -# If the GROUP_GRAPHS and HAVE_DOT tags are set to YES then doxygen -# will generate a graph for groups, showing the direct groups dependencies - -GROUP_GRAPHS = YES - -# If the UML_LOOK tag is set to YES doxygen will generate inheritance and -# collaboration diagrams in a style similar to the OMG's Unified Modeling -# Language. - -UML_LOOK = YES - -# If set to YES, the inheritance and collaboration graphs will show the -# relations between templates and their instances. - -TEMPLATE_RELATIONS = NO - -# If the ENABLE_PREPROCESSING, SEARCH_INCLUDES, INCLUDE_GRAPH, and HAVE_DOT -# tags are set to YES then doxygen will generate a graph for each documented -# file showing the direct and indirect include dependencies of the file with -# other documented files. - -INCLUDE_GRAPH = YES - -# If the ENABLE_PREPROCESSING, SEARCH_INCLUDES, INCLUDED_BY_GRAPH, and -# HAVE_DOT tags are set to YES then doxygen will generate a graph for each -# documented header file showing the documented files that directly or -# indirectly include this file. - -INCLUDED_BY_GRAPH = YES - -# If the CALL_GRAPH and HAVE_DOT options are set to YES then -# doxygen will generate a call dependency graph for every global function -# or class method. Note that enabling this option will significantly increase -# the time of a run. So in most cases it will be better to enable call graphs -# for selected functions only using the \callgraph command. - -CALL_GRAPH = NO - -# If the CALLER_GRAPH and HAVE_DOT tags are set to YES then -# doxygen will generate a caller dependency graph for every global function -# or class method. Note that enabling this option will significantly increase -# the time of a run. So in most cases it will be better to enable caller -# graphs for selected functions only using the \callergraph command. - -CALLER_GRAPH = NO - -# If the GRAPHICAL_HIERARCHY and HAVE_DOT tags are set to YES then doxygen -# will graphical hierarchy of all classes instead of a textual one. - -GRAPHICAL_HIERARCHY = YES - -# If the DIRECTORY_GRAPH, SHOW_DIRECTORIES and HAVE_DOT tags are set to YES -# then doxygen will show the dependencies a directory has on other directories -# in a graphical way. The dependency relations are determined by the #include -# relations between the files in the directories. - -DIRECTORY_GRAPH = YES - -# The DOT_IMAGE_FORMAT tag can be used to set the image format of the images -# generated by dot. Possible values are png, jpg, or gif -# If left blank png will be used. - -DOT_IMAGE_FORMAT = png - -# The tag DOT_PATH can be used to specify the path where the dot tool can be -# found. If left blank, it is assumed the dot tool can be found in the path. - -DOT_PATH = - -# The DOTFILE_DIRS tag can be used to specify one or more directories that -# contain dot files that are included in the documentation (see the -# \dotfile command). - -DOTFILE_DIRS = - -# The DOT_GRAPH_MAX_NODES tag can be used to set the maximum number of -# nodes that will be shown in the graph. If the number of nodes in a graph -# becomes larger than this value, doxygen will truncate the graph, which is -# visualized by representing a node as a red box. Note that doxygen if the -# number of direct children of the root node in a graph is already larger than -# DOT_GRAPH_MAX_NODES then the graph will not be shown at all. Also note -# that the size of a graph can be further restricted by MAX_DOT_GRAPH_DEPTH. - -DOT_GRAPH_MAX_NODES = 50 - -# The MAX_DOT_GRAPH_DEPTH tag can be used to set the maximum depth of the -# graphs generated by dot. A depth value of 3 means that only nodes reachable -# from the root by following a path via at most 3 edges will be shown. Nodes -# that lay further from the root node will be omitted. Note that setting this -# option to 1 or 2 may greatly reduce the computation time needed for large -# code bases. Also note that the size of a graph can be further restricted by -# DOT_GRAPH_MAX_NODES. Using a depth of 0 means no depth restriction. - -MAX_DOT_GRAPH_DEPTH = 0 - -# Set the DOT_TRANSPARENT tag to YES to generate images with a transparent -# background. This is disabled by default, because dot on Windows does not -# seem to support this out of the box. Warning: Depending on the platform used, -# enabling this option may lead to badly anti-aliased labels on the edges of -# a graph (i.e. they become hard to read). - -DOT_TRANSPARENT = NO - -# Set the DOT_MULTI_TARGETS tag to YES allow dot to generate multiple output -# files in one run (i.e. multiple -o and -T options on the command line). This -# makes dot run faster, but since only newer versions of dot (>1.8.10) -# support this, this feature is disabled by default. - -DOT_MULTI_TARGETS = NO - -# If the GENERATE_LEGEND tag is set to YES (the default) Doxygen will -# generate a legend page explaining the meaning of the various boxes and -# arrows in the dot generated graphs. - -GENERATE_LEGEND = YES - -# If the DOT_CLEANUP tag is set to YES (the default) Doxygen will -# remove the intermediate dot files that are used to generate -# the various graphs. - -DOT_CLEANUP = YES - -#--------------------------------------------------------------------------- -# Options related to the search engine -#--------------------------------------------------------------------------- - -# The SEARCHENGINE tag specifies whether or not a search engine should be -# used. If set to NO the values of all tags below this one will be ignored. - -SEARCHENGINE = NO diff --git a/apps/bsatool/bsatool.cpp b/apps/bsatool/bsatool.cpp index 3781dd066..7e47d0b8f 100644 --- a/apps/bsatool/bsatool.cpp +++ b/apps/bsatool/bsatool.cpp @@ -51,7 +51,7 @@ bool parseOptions (int argc, char** argv, Arguments &info) ("help,h", "print help message.") ("version,v", "print version information and quit.") ("long,l", "Include extra information in archive listing.") - ("full-path,f", "Create diretory hierarchy on file extraction " + ("full-path,f", "Create directory hierarchy on file extraction " "(always true for extractall).") ; diff --git a/apps/esmtool/esmtool.cpp b/apps/esmtool/esmtool.cpp index eef96c8c9..ea908590a 100644 --- a/apps/esmtool/esmtool.cpp +++ b/apps/esmtool/esmtool.cpp @@ -249,6 +249,9 @@ void loadCell(ESM::Cell &cell, ESM::ESMReader &esm, Arguments& info) std::cout << " Refnum: " << ref.mRefNum.mIndex << std::endl; std::cout << " ID: '" << ref.mRefID << "'\n"; std::cout << " Owner: '" << ref.mOwner << "'\n"; + std::cout << " Global: '" << ref.mGlobalVariable << "'" << std::endl; + std::cout << " Faction: '" << ref.mFaction << "'" << std::endl; + std::cout << " Faction rank: '" << ref.mFactionRank << "'" << std::endl; std::cout << " Enchantment charge: '" << ref.mEnchantmentCharge << "'\n"; std::cout << " Uses/health: '" << ref.mCharge << "'\n"; std::cout << " Gold value: '" << ref.mGoldValue << "'\n"; diff --git a/apps/esmtool/labels.cpp b/apps/esmtool/labels.cpp index ef45989ef..9543628f5 100644 --- a/apps/esmtool/labels.cpp +++ b/apps/esmtool/labels.cpp @@ -18,137 +18,143 @@ std::string bodyPartLabel(int idx) { - const char *bodyPartLabels[] = { - "Head", - "Hair", - "Neck", - "Cuirass", - "Groin", - "Skirt", - "Right Hand", - "Left Hand", - "Right Wrist", - "Left Wrist", - "Shield", - "Right Forearm", - "Left Forearm", - "Right Upperarm", - "Left Upperarm", - "Right Foot", - "Left Foot", - "Right Ankle", - "Left Ankle", - "Right Knee", - "Left Knee", - "Right Leg", - "Left Leg", - "Right Shoulder", - "Left Shoulder", - "Weapon", - "Tail" - }; - if (idx >= 0 && idx <= 26) + { + const char *bodyPartLabels[] = { + "Head", + "Hair", + "Neck", + "Cuirass", + "Groin", + "Skirt", + "Right Hand", + "Left Hand", + "Right Wrist", + "Left Wrist", + "Shield", + "Right Forearm", + "Left Forearm", + "Right Upperarm", + "Left Upperarm", + "Right Foot", + "Left Foot", + "Right Ankle", + "Left Ankle", + "Right Knee", + "Left Knee", + "Right Leg", + "Left Leg", + "Right Shoulder", + "Left Shoulder", + "Weapon", + "Tail" + }; return bodyPartLabels[idx]; + } else return "Invalid"; } std::string meshPartLabel(int idx) { - const char *meshPartLabels[] = { - "Head", - "Hair", - "Neck", - "Chest", - "Groin", - "Hand", - "Wrist", - "Forearm", - "Upperarm", - "Foot", - "Ankle", - "Knee", - "Upper Leg", - "Clavicle", - "Tail" - }; - if (idx >= 0 && idx <= ESM::BodyPart::MP_Tail) + { + const char *meshPartLabels[] = { + "Head", + "Hair", + "Neck", + "Chest", + "Groin", + "Hand", + "Wrist", + "Forearm", + "Upperarm", + "Foot", + "Ankle", + "Knee", + "Upper Leg", + "Clavicle", + "Tail" + }; return meshPartLabels[idx]; + } else return "Invalid"; } std::string meshTypeLabel(int idx) { - const char *meshTypeLabels[] = { - "Skin", - "Clothing", - "Armor" - }; - if (idx >= 0 && idx <= ESM::BodyPart::MT_Armor) + { + const char *meshTypeLabels[] = { + "Skin", + "Clothing", + "Armor" + }; return meshTypeLabels[idx]; + } else return "Invalid"; } std::string clothingTypeLabel(int idx) { - const char *clothingTypeLabels[] = { - "Pants", - "Shoes", - "Shirt", - "Belt", - "Robe", - "Right Glove", - "Left Glove", - "Skirt", - "Ring", - "Amulet" - }; - if (idx >= 0 && idx <= 9) + { + const char *clothingTypeLabels[] = { + "Pants", + "Shoes", + "Shirt", + "Belt", + "Robe", + "Right Glove", + "Left Glove", + "Skirt", + "Ring", + "Amulet" + }; return clothingTypeLabels[idx]; + } else return "Invalid"; } std::string armorTypeLabel(int idx) -{ - const char *armorTypeLabels[] = { - "Helmet", - "Cuirass", - "Left Pauldron", - "Right Pauldron", - "Greaves", - "Boots", - "Left Gauntlet", - "Right Gauntlet", - "Shield", - "Left Bracer", - "Right Bracer" - }; - +{ if (idx >= 0 && idx <= 10) + { + const char *armorTypeLabels[] = { + "Helmet", + "Cuirass", + "Left Pauldron", + "Right Pauldron", + "Greaves", + "Boots", + "Left Gauntlet", + "Right Gauntlet", + "Shield", + "Left Bracer", + "Right Bracer" + }; return armorTypeLabels[idx]; + } else return "Invalid"; } std::string dialogTypeLabel(int idx) { - const char *dialogTypeLabels[] = { - "Topic", - "Voice", - "Greeting", - "Persuasion", - "Journal" - }; - if (idx >= 0 && idx <= 4) + { + const char *dialogTypeLabels[] = { + "Topic", + "Voice", + "Greeting", + "Persuasion", + "Journal" + }; return dialogTypeLabels[idx]; + } else if (idx == -1) return "Deleted"; else @@ -157,75 +163,79 @@ std::string dialogTypeLabel(int idx) std::string questStatusLabel(int idx) { - const char *questStatusLabels[] = { - "None", - "Name", - "Finished", - "Restart", - "Deleted" - }; - if (idx >= 0 && idx <= 4) + { + const char *questStatusLabels[] = { + "None", + "Name", + "Finished", + "Restart", + "Deleted" + }; return questStatusLabels[idx]; + } else return "Invalid"; } std::string creatureTypeLabel(int idx) { - const char *creatureTypeLabels[] = { - "Creature", - "Daedra", - "Undead", - "Humanoid", - }; - if (idx >= 0 && idx <= 3) + { + const char *creatureTypeLabels[] = { + "Creature", + "Daedra", + "Undead", + "Humanoid", + }; return creatureTypeLabels[idx]; + } else return "Invalid"; } std::string soundTypeLabel(int idx) { - const char *soundTypeLabels[] = { - "Left Foot", - "Right Foot", - "Swim Left", - "Swim Right", - "Moan", - "Roar", - "Scream", - "Land" - }; - if (idx >= 0 && idx <= 7) + { + const char *soundTypeLabels[] = { + "Left Foot", + "Right Foot", + "Swim Left", + "Swim Right", + "Moan", + "Roar", + "Scream", + "Land" + }; return soundTypeLabels[idx]; + } else return "Invalid"; } std::string weaponTypeLabel(int idx) { - const char *weaponTypeLabels[] = { - "Short Blade One Hand", - "Long Blade One Hand", - "Long Blade Two Hand", - "Blunt One Hand", - "Blunt Two Close", - "Blunt Two Wide", - "Spear Two Wide", - "Axe One Hand", - "Axe Two Hand", - "Marksman Bow", - "Marksman Crossbow", - "Marksman Thrown", - "Arrow", - "Bolt" - }; - if (idx >= 0 && idx <= 13) + { + const char *weaponTypeLabels[] = { + "Short Blade One Hand", + "Long Blade One Hand", + "Long Blade Two Hand", + "Blunt One Hand", + "Blunt Two Close", + "Blunt Two Wide", + "Spear Two Wide", + "Axe One Hand", + "Axe Two Hand", + "Marksman Bow", + "Marksman Crossbow", + "Marksman Thrown", + "Arrow", + "Bolt" + }; return weaponTypeLabels[idx]; + } else return "Invalid"; } @@ -242,377 +252,397 @@ std::string aiTypeLabel(int type) std::string magicEffectLabel(int idx) { - const char* magicEffectLabels [] = { - "Water Breathing", - "Swift Swim", - "Water Walking", - "Shield", - "Fire Shield", - "Lightning Shield", - "Frost Shield", - "Burden", - "Feather", - "Jump", - "Levitate", - "SlowFall", - "Lock", - "Open", - "Fire Damage", - "Shock Damage", - "Frost Damage", - "Drain Attribute", - "Drain Health", - "Drain Magicka", - "Drain Fatigue", - "Drain Skill", - "Damage Attribute", - "Damage Health", - "Damage Magicka", - "Damage Fatigue", - "Damage Skill", - "Poison", - "Weakness to Fire", - "Weakness to Frost", - "Weakness to Shock", - "Weakness to Magicka", - "Weakness to Common Disease", - "Weakness to Blight Disease", - "Weakness to Corprus Disease", - "Weakness to Poison", - "Weakness to Normal Weapons", - "Disintegrate Weapon", - "Disintegrate Armor", - "Invisibility", - "Chameleon", - "Light", - "Sanctuary", - "Night Eye", - "Charm", - "Paralyze", - "Silence", - "Blind", - "Sound", - "Calm Humanoid", - "Calm Creature", - "Frenzy Humanoid", - "Frenzy Creature", - "Demoralize Humanoid", - "Demoralize Creature", - "Rally Humanoid", - "Rally Creature", - "Dispel", - "Soultrap", - "Telekinesis", - "Mark", - "Recall", - "Divine Intervention", - "Almsivi Intervention", - "Detect Animal", - "Detect Enchantment", - "Detect Key", - "Spell Absorption", - "Reflect", - "Cure Common Disease", - "Cure Blight Disease", - "Cure Corprus Disease", - "Cure Poison", - "Cure Paralyzation", - "Restore Attribute", - "Restore Health", - "Restore Magicka", - "Restore Fatigue", - "Restore Skill", - "Fortify Attribute", - "Fortify Health", - "Fortify Magicka", - "Fortify Fatigue", - "Fortify Skill", - "Fortify Maximum Magicka", - "Absorb Attribute", - "Absorb Health", - "Absorb Magicka", - "Absorb Fatigue", - "Absorb Skill", - "Resist Fire", - "Resist Frost", - "Resist Shock", - "Resist Magicka", - "Resist Common Disease", - "Resist Blight Disease", - "Resist Corprus Disease", - "Resist Poison", - "Resist Normal Weapons", - "Resist Paralysis", - "Remove Curse", - "Turn Undead", - "Summon Scamp", - "Summon Clannfear", - "Summon Daedroth", - "Summon Dremora", - "Summon Ancestral Ghost", - "Summon Skeletal Minion", - "Summon Bonewalker", - "Summon Greater Bonewalker", - "Summon Bonelord", - "Summon Winged Twilight", - "Summon Hunger", - "Summon Golden Saint", - "Summon Flame Atronach", - "Summon Frost Atronach", - "Summon Storm Atronach", - "Fortify Attack", - "Command Creature", - "Command Humanoid", - "Bound Dagger", - "Bound Longsword", - "Bound Mace", - "Bound Battle Axe", - "Bound Spear", - "Bound Longbow", - "EXTRA SPELL", - "Bound Cuirass", - "Bound Helm", - "Bound Boots", - "Bound Shield", - "Bound Gloves", - "Corprus", - "Vampirism", - "Summon Centurion Sphere", - "Sun Damage", - "Stunted Magicka", - "Summon Fabricant", - "sEffectSummonCreature01", - "sEffectSummonCreature02", - "sEffectSummonCreature03", - "sEffectSummonCreature04", - "sEffectSummonCreature05" - }; if (idx >= 0 && idx <= 142) + { + const char* magicEffectLabels [] = { + "Water Breathing", + "Swift Swim", + "Water Walking", + "Shield", + "Fire Shield", + "Lightning Shield", + "Frost Shield", + "Burden", + "Feather", + "Jump", + "Levitate", + "SlowFall", + "Lock", + "Open", + "Fire Damage", + "Shock Damage", + "Frost Damage", + "Drain Attribute", + "Drain Health", + "Drain Magicka", + "Drain Fatigue", + "Drain Skill", + "Damage Attribute", + "Damage Health", + "Damage Magicka", + "Damage Fatigue", + "Damage Skill", + "Poison", + "Weakness to Fire", + "Weakness to Frost", + "Weakness to Shock", + "Weakness to Magicka", + "Weakness to Common Disease", + "Weakness to Blight Disease", + "Weakness to Corprus Disease", + "Weakness to Poison", + "Weakness to Normal Weapons", + "Disintegrate Weapon", + "Disintegrate Armor", + "Invisibility", + "Chameleon", + "Light", + "Sanctuary", + "Night Eye", + "Charm", + "Paralyze", + "Silence", + "Blind", + "Sound", + "Calm Humanoid", + "Calm Creature", + "Frenzy Humanoid", + "Frenzy Creature", + "Demoralize Humanoid", + "Demoralize Creature", + "Rally Humanoid", + "Rally Creature", + "Dispel", + "Soultrap", + "Telekinesis", + "Mark", + "Recall", + "Divine Intervention", + "Almsivi Intervention", + "Detect Animal", + "Detect Enchantment", + "Detect Key", + "Spell Absorption", + "Reflect", + "Cure Common Disease", + "Cure Blight Disease", + "Cure Corprus Disease", + "Cure Poison", + "Cure Paralyzation", + "Restore Attribute", + "Restore Health", + "Restore Magicka", + "Restore Fatigue", + "Restore Skill", + "Fortify Attribute", + "Fortify Health", + "Fortify Magicka", + "Fortify Fatigue", + "Fortify Skill", + "Fortify Maximum Magicka", + "Absorb Attribute", + "Absorb Health", + "Absorb Magicka", + "Absorb Fatigue", + "Absorb Skill", + "Resist Fire", + "Resist Frost", + "Resist Shock", + "Resist Magicka", + "Resist Common Disease", + "Resist Blight Disease", + "Resist Corprus Disease", + "Resist Poison", + "Resist Normal Weapons", + "Resist Paralysis", + "Remove Curse", + "Turn Undead", + "Summon Scamp", + "Summon Clannfear", + "Summon Daedroth", + "Summon Dremora", + "Summon Ancestral Ghost", + "Summon Skeletal Minion", + "Summon Bonewalker", + "Summon Greater Bonewalker", + "Summon Bonelord", + "Summon Winged Twilight", + "Summon Hunger", + "Summon Golden Saint", + "Summon Flame Atronach", + "Summon Frost Atronach", + "Summon Storm Atronach", + "Fortify Attack", + "Command Creature", + "Command Humanoid", + "Bound Dagger", + "Bound Longsword", + "Bound Mace", + "Bound Battle Axe", + "Bound Spear", + "Bound Longbow", + "EXTRA SPELL", + "Bound Cuirass", + "Bound Helm", + "Bound Boots", + "Bound Shield", + "Bound Gloves", + "Corprus", + "Vampirism", + "Summon Centurion Sphere", + "Sun Damage", + "Stunted Magicka", + "Summon Fabricant", + "sEffectSummonCreature01", + "sEffectSummonCreature02", + "sEffectSummonCreature03", + "sEffectSummonCreature04", + "sEffectSummonCreature05" + }; return magicEffectLabels[idx]; + } else return "Invalid"; } std::string attributeLabel(int idx) { - const char* attributeLabels [] = { - "Strength", - "Intelligence", - "Willpower", - "Agility", - "Speed", - "Endurance", - "Personality", - "Luck" - }; if (idx >= 0 && idx <= 7) + { + const char* attributeLabels [] = { + "Strength", + "Intelligence", + "Willpower", + "Agility", + "Speed", + "Endurance", + "Personality", + "Luck" + }; return attributeLabels[idx]; + } else return "Invalid"; } std::string spellTypeLabel(int idx) { - const char* spellTypeLabels [] = { - "Spells", - "Abilities", - "Blight Disease", - "Disease", - "Curse", - "Powers" - }; if (idx >= 0 && idx <= 5) + { + const char* spellTypeLabels [] = { + "Spells", + "Abilities", + "Blight Disease", + "Disease", + "Curse", + "Powers" + }; return spellTypeLabels[idx]; + } else return "Invalid"; } std::string specializationLabel(int idx) { - const char* specializationLabels [] = { - "Combat", - "Magic", - "Stealth" - }; if (idx >= 0 && idx <= 2) + { + const char* specializationLabels [] = { + "Combat", + "Magic", + "Stealth" + }; return specializationLabels[idx]; + } else return "Invalid"; } std::string skillLabel(int idx) { - const char* skillLabels [] = { - "Block", - "Armorer", - "Medium Armor", - "Heavy Armor", - "Blunt Weapon", - "Long Blade", - "Axe", - "Spear", - "Athletics", - "Enchant", - "Destruction", - "Alteration", - "Illusion", - "Conjuration", - "Mysticism", - "Restoration", - "Alchemy", - "Unarmored", - "Security", - "Sneak", - "Acrobatics", - "Light Armor", - "Short Blade", - "Marksman", - "Mercantile", - "Speechcraft", - "Hand-to-hand" - }; if (idx >= 0 && idx <= 26) + { + const char* skillLabels [] = { + "Block", + "Armorer", + "Medium Armor", + "Heavy Armor", + "Blunt Weapon", + "Long Blade", + "Axe", + "Spear", + "Athletics", + "Enchant", + "Destruction", + "Alteration", + "Illusion", + "Conjuration", + "Mysticism", + "Restoration", + "Alchemy", + "Unarmored", + "Security", + "Sneak", + "Acrobatics", + "Light Armor", + "Short Blade", + "Marksman", + "Mercantile", + "Speechcraft", + "Hand-to-hand" + }; return skillLabels[idx]; + } else return "Invalid"; } std::string apparatusTypeLabel(int idx) { - const char* apparatusTypeLabels [] = { - "Mortar", - "Alembic", - "Calcinator", - "Retort", - }; if (idx >= 0 && idx <= 3) + { + const char* apparatusTypeLabels [] = { + "Mortar", + "Alembic", + "Calcinator", + "Retort", + }; return apparatusTypeLabels[idx]; + } else return "Invalid"; } std::string rangeTypeLabel(int idx) { - const char* rangeTypeLabels [] = { - "Self", - "Touch", - "Target" - }; if (idx >= 0 && idx <= 2) + { + const char* rangeTypeLabels [] = { + "Self", + "Touch", + "Target" + }; return rangeTypeLabels[idx]; + } else return "Invalid"; } std::string schoolLabel(int idx) { - const char* schoolLabels [] = { - "Alteration", - "Conjuration", - "Destruction", - "Illusion", - "Mysticism", - "Restoration" - }; if (idx >= 0 && idx <= 5) + { + const char* schoolLabels [] = { + "Alteration", + "Conjuration", + "Destruction", + "Illusion", + "Mysticism", + "Restoration" + }; return schoolLabels[idx]; + } else return "Invalid"; } std::string enchantTypeLabel(int idx) { - const char* enchantTypeLabels [] = { - "Cast Once", - "Cast When Strikes", - "Cast When Used", - "Constant Effect" - }; if (idx >= 0 && idx <= 3) + { + const char* enchantTypeLabels [] = { + "Cast Once", + "Cast When Strikes", + "Cast When Used", + "Constant Effect" + }; return enchantTypeLabels[idx]; + } else return "Invalid"; } std::string ruleFunction(int idx) { - std::string ruleFunctions[] = { - "Reaction Low", - "Reaction High", - "Rank Requirement", - "NPC? Reputation", - "Health Percent", - "Player Reputation", - "NPC Level", - "Player Health Percent", - "Player Magicka", - "Player Fatigue", - "Player Attribute Strength", - "Player Skill Block", - "Player Skill Armorer", - "Player Skill Medium Armor", - "Player Skill Heavy Armor", - "Player Skill Blunt Weapon", - "Player Skill Long Blade", - "Player Skill Axe", - "Player Skill Spear", - "Player Skill Athletics", - "Player Skill Enchant", - "Player Skill Destruction", - "Player Skill Alteration", - "Player Skill Illusion", - "Player Skill Conjuration", - "Player Skill Mysticism", - "Player SKill Restoration", - "Player Skill Alchemy", - "Player Skill Unarmored", - "Player Skill Security", - "Player Skill Sneak", - "Player Skill Acrobatics", - "Player Skill Light Armor", - "Player Skill Short Blade", - "Player Skill Marksman", - "Player Skill Mercantile", - "Player Skill Speechcraft", - "Player Skill Hand to Hand", - "Player Gender", - "Player Expelled from Faction", - "Player Diseased (Common)", - "Player Diseased (Blight)", - "Player Clothing Modifier", - "Player Crime Level", - "Player Same Sex", - "Player Same Race", - "Player Same Faction", - "Faction Rank Difference", - "Player Detected", - "Alarmed", - "Choice Selected", - "Player Attribute Intelligence", - "Player Attribute Willpower", - "Player Attribute Agility", - "Player Attribute Speed", - "Player Attribute Endurance", - "Player Attribute Personality", - "Player Attribute Luck", - "Player Diseased (Corprus)", - "Weather", - "Player is a Vampire", - "Player Level", - "Attacked", - "NPC Talked to Player", - "Player Health", - "Creature Target", - "Friend Hit", - "Fight", - "Hello", - "Alarm", - "Flee", - "Should Attack", - "Werewolf" - }; if (idx >= 0 && idx <= 72) + { + std::string ruleFunctions[] = { + "Reaction Low", + "Reaction High", + "Rank Requirement", + "NPC? Reputation", + "Health Percent", + "Player Reputation", + "NPC Level", + "Player Health Percent", + "Player Magicka", + "Player Fatigue", + "Player Attribute Strength", + "Player Skill Block", + "Player Skill Armorer", + "Player Skill Medium Armor", + "Player Skill Heavy Armor", + "Player Skill Blunt Weapon", + "Player Skill Long Blade", + "Player Skill Axe", + "Player Skill Spear", + "Player Skill Athletics", + "Player Skill Enchant", + "Player Skill Destruction", + "Player Skill Alteration", + "Player Skill Illusion", + "Player Skill Conjuration", + "Player Skill Mysticism", + "Player SKill Restoration", + "Player Skill Alchemy", + "Player Skill Unarmored", + "Player Skill Security", + "Player Skill Sneak", + "Player Skill Acrobatics", + "Player Skill Light Armor", + "Player Skill Short Blade", + "Player Skill Marksman", + "Player Skill Mercantile", + "Player Skill Speechcraft", + "Player Skill Hand to Hand", + "Player Gender", + "Player Expelled from Faction", + "Player Diseased (Common)", + "Player Diseased (Blight)", + "Player Clothing Modifier", + "Player Crime Level", + "Player Same Sex", + "Player Same Race", + "Player Same Faction", + "Faction Rank Difference", + "Player Detected", + "Alarmed", + "Choice Selected", + "Player Attribute Intelligence", + "Player Attribute Willpower", + "Player Attribute Agility", + "Player Attribute Speed", + "Player Attribute Endurance", + "Player Attribute Personality", + "Player Attribute Luck", + "Player Diseased (Corprus)", + "Weather", + "Player is a Vampire", + "Player Level", + "Attacked", + "NPC Talked to Player", + "Player Health", + "Creature Target", + "Friend Hit", + "Fight", + "Hello", + "Alarm", + "Flee", + "Should Attack", + "Werewolf" + }; return ruleFunctions[idx]; + } else return "Invalid"; } @@ -787,6 +817,10 @@ std::string magicEffectFlags(int flags) if (flags & ESM::MagicEffect::NonRecastable) properties += "NonRecastable "; if (flags & ESM::MagicEffect::Unreflectable) properties += "Unreflectable "; if (flags & ESM::MagicEffect::CasterLinked) properties += "CasterLinked "; + if (flags & ESM::MagicEffect::AllowSpellmaking) properties += "AllowSpellmaking "; + if (flags & ESM::MagicEffect::AllowEnchanting) properties += "AllowEnchanting "; + if (flags & ESM::MagicEffect::NegativeLight) properties += "NegativeLight "; + if (flags & 0xFFFC0000) properties += "Invalid "; properties += str(boost::format("(0x%08X)") % flags); return properties; diff --git a/apps/esmtool/record.cpp b/apps/esmtool/record.cpp index bcf16091f..3f2ebc2ba 100644 --- a/apps/esmtool/record.cpp +++ b/apps/esmtool/record.cpp @@ -412,7 +412,7 @@ void Record::print() std::cout << " Armor: " << mData.mData.mArmor << std::endl; std::cout << " Enchantment Points: " << mData.mData.mEnchant << std::endl; std::vector::iterator pit; - for (pit = mData.mParts.mParts.begin(); pit != mData.mParts.mParts.end(); pit++) + for (pit = mData.mParts.mParts.begin(); pit != mData.mParts.mParts.end(); ++pit) { std::cout << " Body Part: " << bodyPartLabel(pit->mPart) << " (" << (int)(pit->mPart) << ")" << std::endl; @@ -484,7 +484,7 @@ void Record::print() std::cout << " Texture: " << mData.mTexture << std::endl; std::cout << " Description: " << mData.mDescription << std::endl; std::vector::iterator pit; - for (pit = mData.mPowers.mList.begin(); pit != mData.mPowers.mList.end(); pit++) + for (pit = mData.mPowers.mList.begin(); pit != mData.mPowers.mList.end(); ++pit) std::cout << " Power: " << *pit << std::endl; } @@ -513,7 +513,7 @@ void Record::print() else std::cout << " Map Color: " << boost::format("0x%08X") % mData.mMapColor << std::endl; std::cout << " Water Level Int: " << mData.mWaterInt << std::endl; - std::cout << " NAM0: " << mData.mNAM0 << std::endl; + std::cout << " RefId counter: " << mData.mRefNumCounter << std::endl; } @@ -554,7 +554,7 @@ void Record::print() std::cout << " Value: " << mData.mData.mValue << std::endl; std::cout << " Enchantment Points: " << mData.mData.mEnchant << std::endl; std::vector::iterator pit; - for (pit = mData.mParts.mParts.begin(); pit != mData.mParts.mParts.end(); pit++) + for (pit = mData.mParts.mParts.begin(); pit != mData.mParts.mParts.end(); ++pit) { std::cout << " Body Part: " << bodyPartLabel(pit->mPart) << " (" << (int)(pit->mPart) << ")" << std::endl; @@ -574,7 +574,7 @@ void Record::print() std::cout << " Flags: " << containerFlags(mData.mFlags) << std::endl; std::cout << " Weight: " << mData.mWeight << std::endl; std::vector::iterator cit; - for (cit = mData.mInventory.mList.begin(); cit != mData.mInventory.mList.end(); cit++) + for (cit = mData.mInventory.mList.begin(); cit != mData.mInventory.mList.end(); ++cit) std::cout << " Inventory: Count: " << boost::format("%4d") % cit->mCount << " Item: " << cit->mItem.toString() << std::endl; } @@ -619,12 +619,12 @@ void Record::print() std::cout << " Gold: " << mData.mData.mGold << std::endl; std::vector::iterator cit; - for (cit = mData.mInventory.mList.begin(); cit != mData.mInventory.mList.end(); cit++) + for (cit = mData.mInventory.mList.begin(); cit != mData.mInventory.mList.end(); ++cit) std::cout << " Inventory: Count: " << boost::format("%4d") % cit->mCount << " Item: " << cit->mItem.toString() << std::endl; std::vector::iterator sit; - for (sit = mData.mSpells.mList.begin(); sit != mData.mSpells.mList.end(); sit++) + for (sit = mData.mSpells.mList.begin(); sit != mData.mSpells.mList.end(); ++sit) std::cout << " Spell: " << *sit << std::endl; std::cout << " Artifical Intelligence: " << mData.mHasAI << std::endl; @@ -639,7 +639,7 @@ void Record::print() std::cout << " AI Services:" << boost::format("0x%08X") % mData.mAiData.mServices << std::endl; std::vector::iterator pit; - for (pit = mData.mAiPackage.mList.begin(); pit != mData.mAiPackage.mList.end(); pit++) + for (pit = mData.mAiPackage.mList.begin(); pit != mData.mAiPackage.mList.end(); ++pit) printAIPackage(*pit); } @@ -682,13 +682,11 @@ void Record::print() { std::cout << " Name: " << mData.mName << std::endl; std::cout << " Hidden: " << mData.mData.mIsHidden << std::endl; - if (mData.mData.mUnknown != -1) - std::cout << " Unknown: " << mData.mData.mUnknown << std::endl; std::cout << " Attribute1: " << attributeLabel(mData.mData.mAttribute[0]) << " (" << mData.mData.mAttribute[0] << ")" << std::endl; std::cout << " Attribute2: " << attributeLabel(mData.mData.mAttribute[1]) << " (" << mData.mData.mAttribute[1] << ")" << std::endl; - for (int i = 0; i != 6; i++) + for (int i = 0; i < 7; i++) if (mData.mData.mSkills[i] != -1) std::cout << " Skill: " << skillLabel(mData.mData.mSkills[i]) << " (" << mData.mData.mSkills[i] << ")" << std::endl; @@ -708,7 +706,7 @@ void Record::print() << mData.mData.mRankData[i].mFactReaction << std::endl; } std::map::iterator rit; - for (rit = mData.mReactions.begin(); rit != mData.mReactions.end(); rit++) + for (rit = mData.mReactions.begin(); rit != mData.mReactions.end(); ++rit) std::cout << " Reaction: " << rit->second << " = " << rit->first << std::endl; } @@ -765,7 +763,7 @@ void Record::print() std::cout << " Unknown2: " << (int)mData.mData.mUnknown2 << std::endl; std::vector::iterator sit; - for (sit = mData.mSelects.begin(); sit != mData.mSelects.end(); sit++) + for (sit = mData.mSelects.begin(); sit != mData.mSelects.end(); ++sit) std::cout << " Select Rule: " << ruleString(*sit) << std::endl; if (mData.mResultScript != "") @@ -837,7 +835,7 @@ void Record::print() std::cout << " Flags: " << creatureListFlags(mData.mFlags) << std::endl; std::cout << " Number of items: " << mData.mList.size() << std::endl; std::vector::iterator iit; - for (iit = mData.mList.begin(); iit != mData.mList.end(); iit++) + for (iit = mData.mList.begin(); iit != mData.mList.end(); ++iit) std::cout << " Creature: Level: " << iit->mLevel << " Creature: " << iit->mId << std::endl; } @@ -849,7 +847,7 @@ void Record::print() std::cout << " Flags: " << itemListFlags(mData.mFlags) << std::endl; std::cout << " Number of items: " << mData.mList.size() << std::endl; std::vector::iterator iit; - for (iit = mData.mList.begin(); iit != mData.mList.end(); iit++) + for (iit = mData.mList.begin(); iit != mData.mList.end(); ++iit) std::cout << " Inventory: Level: " << iit->mLevel << " Item: " << iit->mId << std::endl; } @@ -952,9 +950,9 @@ void Record::print() std::cout << " School: " << schoolLabel(mData.mData.mSchool) << " (" << mData.mData.mSchool << ")" << std::endl; std::cout << " Base Cost: " << mData.mData.mBaseCost << std::endl; + std::cout << " Unknown 1: " << mData.mData.mUnknown1 << std::endl; std::cout << " Speed: " << mData.mData.mSpeed << std::endl; - std::cout << " Size: " << mData.mData.mSize << std::endl; - std::cout << " Size Cap: " << mData.mData.mSizeCap << std::endl; + std::cout << " Unknown 2: " << mData.mData.mUnknown2 << std::endl; std::cout << " RGB Color: " << "(" << mData.mData.mRed << "," << mData.mData.mGreen << "," @@ -994,7 +992,6 @@ void Record::print() std::cout << " Level: " << mData.mNpdt12.mLevel << std::endl; std::cout << " Reputation: " << (int)mData.mNpdt12.mReputation << std::endl; std::cout << " Disposition: " << (int)mData.mNpdt12.mDisposition << std::endl; - std::cout << " Faction: " << (int)mData.mNpdt52.mFactionID << std::endl; std::cout << " Rank: " << (int)mData.mNpdt12.mRank << std::endl; std::cout << " Unknown1: " << (unsigned int)((unsigned char)mData.mNpdt12.mUnknown1) << std::endl; @@ -1009,6 +1006,7 @@ void Record::print() std::cout << " Reputation: " << (int)mData.mNpdt52.mReputation << std::endl; std::cout << " Disposition: " << (int)mData.mNpdt52.mDisposition << std::endl; std::cout << " Rank: " << (int)mData.mNpdt52.mRank << std::endl; + std::cout << " FactionID: " << (int)mData.mNpdt52.mFactionID << std::endl; std::cout << " Attributes:" << std::endl; std::cout << " Strength: " << (int)mData.mNpdt52.mStrength << std::endl; @@ -1033,16 +1031,16 @@ void Record::print() } std::vector::iterator cit; - for (cit = mData.mInventory.mList.begin(); cit != mData.mInventory.mList.end(); cit++) + for (cit = mData.mInventory.mList.begin(); cit != mData.mInventory.mList.end(); ++cit) std::cout << " Inventory: Count: " << boost::format("%4d") % cit->mCount << " Item: " << cit->mItem.toString() << std::endl; std::vector::iterator sit; - for (sit = mData.mSpells.mList.begin(); sit != mData.mSpells.mList.end(); sit++) + for (sit = mData.mSpells.mList.begin(); sit != mData.mSpells.mList.end(); ++sit) std::cout << " Spell: " << *sit << std::endl; std::vector::iterator dit; - for (dit = mData.mTransport.begin(); dit != mData.mTransport.end(); dit++) + for (dit = mData.mTransport.begin(); dit != mData.mTransport.end(); ++dit) { std::cout << " Destination Position: " << boost::format("%12.3f") % dit->mPos.pos[0] << "," @@ -1068,7 +1066,7 @@ void Record::print() std::cout << " AI Services:" << boost::format("0x%08X") % mData.mAiData.mServices << std::endl; std::vector::iterator pit; - for (pit = mData.mAiPackage.mList.begin(); pit != mData.mAiPackage.mList.end(); pit++) + for (pit = mData.mAiPackage.mList.begin(); pit != mData.mAiPackage.mList.end(); ++pit) printAIPackage(*pit); } @@ -1142,7 +1140,7 @@ void Record::print() << mData.mData.mBonus[i].mBonus << std::endl; std::vector::iterator sit; - for (sit = mData.mPowers.mList.begin(); sit != mData.mPowers.mList.end(); sit++) + for (sit = mData.mPowers.mList.begin(); sit != mData.mPowers.mList.end(); ++sit) std::cout << " Power: " << *sit << std::endl; } @@ -1166,7 +1164,7 @@ void Record::print() if (mData.mSleepList != "") std::cout << " Sleep List: " << mData.mSleepList << std::endl; std::vector::iterator sit; - for (sit = mData.mSoundList.begin(); sit != mData.mSoundList.end(); sit++) + for (sit = mData.mSoundList.begin(); sit != mData.mSoundList.end(); ++sit) std::cout << " Sound: " << (int)sit->mChance << " = " << sit->mSound.toString() << std::endl; } @@ -1183,12 +1181,12 @@ void Record::print() std::vector::iterator vit; - for (vit = mData.mVarNames.begin(); vit != mData.mVarNames.end(); vit++) + for (vit = mData.mVarNames.begin(); vit != mData.mVarNames.end(); ++vit) std::cout << " Variable: " << *vit << std::endl; std::cout << " ByteCode: "; std::vector::iterator cit; - for (cit = mData.mScriptData.begin(); cit != mData.mScriptData.end(); cit++) + for (cit = mData.mScriptData.begin(); cit != mData.mScriptData.end(); ++cit) std::cout << boost::format("%02X") % (int)(*cit); std::cout << std::endl; diff --git a/apps/launcher/CMakeLists.txt b/apps/launcher/CMakeLists.txt index eb11cab49..ba330f70c 100644 --- a/apps/launcher/CMakeLists.txt +++ b/apps/launcher/CMakeLists.txt @@ -13,7 +13,7 @@ set(LAUNCHER utils/textinputdialog.cpp utils/lineedit.cpp - ${CMAKE_SOURCE_DIR}/files/launcher/launcher.rc + ${CMAKE_SOURCE_DIR}/files/windows/launcher.rc ) set(LAUNCHER_HEADER @@ -74,19 +74,10 @@ QT4_WRAP_UI(UI_HDRS ${LAUNCHER_UI}) include(${QT_USE_FILE}) include_directories(${CMAKE_CURRENT_BINARY_DIR}) if(NOT WIN32) - include_directories(${LIBUNSHIELD_INCLUDE}) + include_directories(${LIBUNSHIELD_INCLUDE_DIR}) endif(NOT WIN32) # Main executable -IF(OGRE_STATIC) -IF(WIN32) -ADD_DEFINITIONS(-DENABLE_PLUGIN_Direct3D9 -DENABLE_PLUGIN_GL) -set(OGRE_STATIC_PLUGINS ${OGRE_RenderSystem_Direct3D9_LIBRARIES} ${OGRE_RenderSystem_GL_LIBRARIES}) -ELSE(WIN32) -ADD_DEFINITIONS(-DENABLE_PLUGIN_GL) -set(OGRE_STATIC_PLUGINS ${OGRE_RenderSystem_GL_LIBRARIES}) -ENDIF(WIN32) -ENDIF(OGRE_STATIC) add_executable(omwlauncher ${GUI_TYPE} ${LAUNCHER} @@ -100,16 +91,13 @@ target_link_libraries(omwlauncher ${Boost_LIBRARIES} ${OGRE_LIBRARIES} ${OGRE_STATIC_PLUGINS} - ${SDL2_LIBRARY} + ${SDL2_LIBRARY_ONLY} ${QT_LIBRARIES} components ) -if(DPKG_PROGRAM) - INSTALL(TARGETS omwlauncher RUNTIME DESTINATION games COMPONENT omwlauncher) -endif() -if(BUILD_WITH_CODE_COVERAGE) +if (BUILD_WITH_CODE_COVERAGE) add_definitions (--coverage) target_link_libraries(omwlauncher gcov) endif() diff --git a/apps/launcher/graphicspage.cpp b/apps/launcher/graphicspage.cpp index 638237f34..1c6e69023 100644 --- a/apps/launcher/graphicspage.cpp +++ b/apps/launcher/graphicspage.cpp @@ -33,7 +33,11 @@ QString getAspect(int x, int y) } Launcher::GraphicsPage::GraphicsPage(Files::ConfigurationManager &cfg, GraphicsSettings &graphicsSetting, QWidget *parent) - : mCfgMgr(cfg) + : mOgre(NULL) + , mSelectedRenderSystem(NULL) + , mOpenGLRenderSystem(NULL) + , mDirect3DRenderSystem(NULL) + , mCfgMgr(cfg) , mGraphicsSettings(graphicsSetting) , QWidget(parent) { diff --git a/apps/mwiniimporter/CMakeLists.txt b/apps/mwiniimporter/CMakeLists.txt index 702f66513..deab88ce2 100644 --- a/apps/mwiniimporter/CMakeLists.txt +++ b/apps/mwiniimporter/CMakeLists.txt @@ -22,8 +22,3 @@ if (BUILD_WITH_CODE_COVERAGE) add_definitions (--coverage) target_link_libraries(mwiniimport gcov) endif() - -if(DPKG_PROGRAM) - INSTALL(TARGETS mwiniimport RUNTIME DESTINATION games COMPONENT mwiniimport) -endif() - diff --git a/apps/mwiniimporter/importer.cpp b/apps/mwiniimporter/importer.cpp index 3a52592ae..80f186b1f 100644 --- a/apps/mwiniimporter/importer.cpp +++ b/apps/mwiniimporter/importer.cpp @@ -15,6 +15,7 @@ namespace bfs = boost::filesystem; MwIniImporter::MwIniImporter() : mVerbose(false) + , mEncoding(ToUTF8::WINDOWS_1250) { const char *map[][2] = { @@ -709,8 +710,7 @@ MwIniImporter::multistrmap MwIniImporter::loadIniFile(const std::string& filenam continue; } - multistrmap::iterator it; - if((it = map.find(key)) == map.end()) { + if(map.find(key) == map.end()) { map.insert( std::make_pair (key, std::vector() ) ); } map[key].push_back(value); @@ -746,8 +746,7 @@ MwIniImporter::multistrmap MwIniImporter::loadCfgFile(const std::string& filenam std::string key(line.substr(0,pos)); std::string value(line.substr(pos+1)); - multistrmap::iterator it; - if((it = map.find(key)) == map.end()) { + if(map.find(key) == map.end()) { map.insert( std::make_pair (key, std::vector() ) ); } map[key].push_back(value); diff --git a/apps/mwiniimporter/main.cpp b/apps/mwiniimporter/main.cpp index c10103cd6..fdf6db804 100644 --- a/apps/mwiniimporter/main.cpp +++ b/apps/mwiniimporter/main.cpp @@ -37,6 +37,8 @@ public: char **get() const { return const_cast(argv); } private: + utf8argv(const utf8argv&); + utf8argv& operator=(const utf8argv&); const char **argv; std::vector args; diff --git a/apps/opencs/CMakeLists.txt b/apps/opencs/CMakeLists.txt index 4576432e1..969058dd7 100644 --- a/apps/opencs/CMakeLists.txt +++ b/apps/opencs/CMakeLists.txt @@ -1,15 +1,17 @@ -set (OPENCS_SRC main.cpp) +set (OPENCS_SRC main.cpp + ${CMAKE_SOURCE_DIR}/files/windows/opencs.rc + ) opencs_units (. editor) set (CMAKE_BUILD_TYPE DEBUG) opencs_units (model/doc - document operation saving documentmanager loader + document operation saving documentmanager loader runner ) opencs_units_noqt (model/doc - stage savingstate savingstages + stage savingstate savingstages blacklist ) opencs_hdrs_noqt (model/doc @@ -18,17 +20,18 @@ opencs_hdrs_noqt (model/doc opencs_units (model/world - idtable idtableproxymodel regionmap data + idtable idtableproxymodel regionmap data commanddispatcher idtablebase resourcetable ) opencs_units_noqt (model/world universalid record commands columnbase scriptcontext cell refidcollection - refidadapter refiddata refidadapterimp ref collectionbase refcollection columns infocollection tablemimedata cellcoordinates cellselection + refidadapter refiddata refidadapterimp ref collectionbase refcollection columns infocollection tablemimedata cellcoordinates cellselection resources resourcesmanager scope + pathgrid landtexture land ) opencs_hdrs_noqt (model/world - columnimp idcollection collection info + columnimp idcollection collection info subcellcollection ) @@ -38,13 +41,13 @@ opencs_units (model/tools opencs_units_noqt (model/tools mandatoryid skillcheck classcheck factioncheck racecheck soundcheck regioncheck - birthsigncheck spellcheck referenceablecheck scriptcheck + birthsigncheck spellcheck referenceablecheck scriptcheck bodypartcheck ) opencs_units (view/doc viewmanager view operations operation subview startup filedialog newgame - filewidget adjusterwidget loader + filewidget adjusterwidget loader globaldebugprofilemenu runlogsubview ) @@ -59,23 +62,31 @@ opencs_hdrs_noqt (view/doc opencs_units (view/world table tablesubview scriptsubview util regionmapsubview tablebottombox creator genericcreator - cellcreator referenceablecreator referencecreator scenesubview scenetoolbar scenetool - scenetoolmode infocreator scriptedit dialoguesubview previewsubview regionmap dragrecordtable + cellcreator referenceablecreator referencecreator scenesubview + infocreator scriptedit dialoguesubview previewsubview regionmap dragrecordtable + ) + +opencs_units_noqt (view/world + subviews enumdelegate vartypedelegate recordstatusdelegate idtypedelegate datadisplaydelegate + scripthighlighter idvalidator dialoguecreator physicssystem physicsmanager + ) + +opencs_units (view/widget + scenetoolbar scenetool scenetoolmode pushbutton scenetooltoggle scenetoolrun modebutton ) opencs_units (view/render scenewidget worldspacewidget pagedworldspacewidget unpagedworldspacewidget - previewwidget + previewwidget editmode ) opencs_units_noqt (view/render navigation navigation1st navigationfree navigationorbit lighting lightingday lightingnight - lightingbright + lightingbright object cell terrainstorage textoverlay overlaymask overlaysystem mousestate ) -opencs_units_noqt (view/world - subviews enumdelegate vartypedelegate recordstatusdelegate idtypedelegate datadisplaydelegate - scripthighlighter idvalidator dialoguecreator +opencs_hdrs_noqt (view/render + elements ) @@ -118,12 +129,8 @@ opencs_units_noqt (model/filter node unarynode narynode leafnode booleannode parser andnode ornode notnode textnode valuenode ) -opencs_hdrs_noqt (model/filter - filter - ) - opencs_units (view/filter - filtercreator filterbox recordfilterbox editwidget + filterbox recordfilterbox editwidget ) set (OPENCS_US @@ -158,7 +165,7 @@ qt4_wrap_ui(OPENCS_UI_HDR ${OPENCS_UI}) qt4_wrap_cpp(OPENCS_MOC_SRC ${OPENCS_HDR_QT}) qt4_add_resources(OPENCS_RES_SRC ${OPENCS_RES}) -include_directories(${CMAKE_CURRENT_BINARY_DIR}) +include_directories(${CMAKE_CURRENT_BINARY_DIR} ${BULLET_INCLUDE_DIRS}) if(APPLE) set (OPENCS_MAC_ICON ${CMAKE_SOURCE_DIR}/files/mac/opencs.icns) @@ -168,6 +175,7 @@ endif(APPLE) add_executable(opencs MACOSX_BUNDLE + ${OENGINE_BULLET} ${OPENCS_SRC} ${OPENCS_UI_HDR} ${OPENCS_MOC_SRC} @@ -192,16 +200,15 @@ endif(APPLE) target_link_libraries(opencs ${OGRE_LIBRARIES} + ${OGRE_Overlay_LIBRARIES} + ${OGRE_STATIC_PLUGINS} ${SHINY_LIBRARIES} ${Boost_LIBRARIES} + ${BULLET_LIBRARIES} ${QT_LIBRARIES} components ) -if(DPKG_PROGRAM) - INSTALL(TARGETS opencs RUNTIME DESTINATION games COMPONENT opencs) -endif() - if(APPLE) INSTALL(TARGETS opencs BUNDLE DESTINATION OpenMW COMPONENT BUNDLE) endif() diff --git a/apps/opencs/editor.cpp b/apps/opencs/editor.cpp index b00373587..591667ebb 100644 --- a/apps/opencs/editor.cpp +++ b/apps/opencs/editor.cpp @@ -20,8 +20,9 @@ #include "model/world/data.hpp" CS::Editor::Editor (OgreInit::OgreInit& ogreInit) -: mUserSettings (mCfgMgr), mDocumentManager (mCfgMgr), mViewManager (mDocumentManager), - mIpcServerName ("org.openmw.OpenCS") +: mUserSettings (mCfgMgr), mOverlaySystem (0), mDocumentManager (mCfgMgr), + mViewManager (mDocumentManager), mPhysicsManager (0), + mIpcServerName ("org.openmw.OpenCS"), mServer(NULL), mClientSocket(NULL) { std::pair > config = readConfig(); @@ -32,9 +33,14 @@ CS::Editor::Editor (OgreInit::OgreInit& ogreInit) ogreInit.init ((mCfgMgr.getUserConfigPath() / "opencsOgre.log").string()); + mOverlaySystem.reset (new CSVRender::OverlaySystem); + mPhysicsManager.reset (new CSVWorld::PhysicsManager); + Bsa::registerResources (Files::Collections (config.first, !mFsStrict), config.second, true, mFsStrict); + mDocumentManager.listResources(); + mNewGame.setLocalData (mLocal); mFileDialog.setLocalData (mLocal); @@ -63,6 +69,9 @@ CS::Editor::Editor (OgreInit::OgreInit& ogreInit) this, SLOT (createNewGame (const boost::filesystem::path&))); } +CS::Editor::~Editor () +{} + void CS::Editor::setupDataFiles (const Files::PathContainer& dataDirs) { for (Files::PathContainer::const_iterator iter = dataDirs.begin(); iter != dataDirs.end(); ++iter) @@ -78,13 +87,17 @@ std::pair > CS::Editor::readConfi boost::program_options::options_description desc("Syntax: opencs \nAllowed options"); desc.add_options() - ("data", boost::program_options::value()->default_value(Files::PathContainer(), "data")->multitoken()) + ("data", boost::program_options::value()->default_value(Files::PathContainer(), "data")->multitoken()->composing()) ("data-local", boost::program_options::value()->default_value("")) ("fs-strict", boost::program_options::value()->implicit_value(true)->default_value(false)) ("encoding", boost::program_options::value()->default_value("win1252")) ("resources", boost::program_options::value()->default_value("resources")) ("fallback-archive", boost::program_options::value >()-> - default_value(std::vector(), "fallback-archive")->multitoken()); + default_value(std::vector(), "fallback-archive")->multitoken()) + ("script-blacklist", boost::program_options::value >()->default_value(std::vector(), "") + ->multitoken(), "exclude specified script from the verifier (if the use of the blacklist is enabled)") + ("script-blacklist-use", boost::program_options::value()->implicit_value(true) + ->default_value(true), "enable script blacklisting"); boost::program_options::notify(variables); @@ -95,6 +108,10 @@ std::pair > CS::Editor::readConfi mDocumentManager.setResourceDir (mResources = variables["resources"].as()); + if (variables["script-blacklist-use"].as()) + mDocumentManager.setBlacklistedScripts ( + variables["script-blacklist"].as >()); + mFsStrict = variables["fs-strict"].as(); Files::PathContainer dataDirs, dataLocal; @@ -179,7 +196,7 @@ void CS::Editor::createNewFile (const boost::filesystem::path &savePath) files.push_back(path.toUtf8().constData()); } - files.push_back(mFileDialog.filename().toUtf8().constData()); + files.push_back (savePath); mDocumentManager.addDocument (files, savePath, true); @@ -249,26 +266,49 @@ int CS::Editor::run() std::auto_ptr CS::Editor::setupGraphics() { - // TODO: setting - Ogre::Root::getSingleton().setRenderSystem(Ogre::Root::getSingleton().getRenderSystemByName("OpenGL Rendering Subsystem")); + std::string renderer = +#if OGRE_PLATFORM == OGRE_PLATFORM_WIN32 + "Direct3D9 Rendering Subsystem"; +#else + "OpenGL Rendering Subsystem"; +#endif + std::string renderSystem = mUserSettings.setting("Video/render system", renderer.c_str()).toStdString(); + + Ogre::Root::getSingleton().setRenderSystem(Ogre::Root::getSingleton().getRenderSystemByName(renderSystem)); + + // Initialise Ogre::OverlaySystem after Ogre::Root but before initialisation + mOverlaySystem.get(); Ogre::Root::getSingleton().initialise(false); // Create a hidden background window to keep resources Ogre::NameValuePairList params; params.insert(std::make_pair("title", "")); - params.insert(std::make_pair("FSAA", "0")); + + std::string antialiasing = mUserSettings.settingValue("Video/antialiasing").toStdString(); + if(antialiasing == "MSAA 16") antialiasing = "16"; + else if(antialiasing == "MSAA 8") antialiasing = "8"; + else if(antialiasing == "MSAA 4") antialiasing = "4"; + else if(antialiasing == "MSAA 2") antialiasing = "2"; + else antialiasing = "0"; + params.insert(std::make_pair("FSAA", antialiasing)); + params.insert(std::make_pair("vsync", "false")); params.insert(std::make_pair("hidden", "true")); #if OGRE_PLATFORM == OGRE_PLATFORM_APPLE params.insert(std::make_pair("macAPI", "cocoa")); #endif + // NOTE: fullscreen mode not supported (doesn't really make sense for opencs) Ogre::RenderWindow* hiddenWindow = Ogre::Root::getSingleton().createRenderWindow("InactiveHidden", 1, 1, false, ¶ms); hiddenWindow->setActive(false); sh::OgrePlatform* platform = new sh::OgrePlatform ("General", (mResources / "materials").string()); + // for font used in overlays + Ogre::Root::getSingleton().addResourceLocation ((mResources / "mygui").string(), + "FileSystem", Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME, true); + if (!boost::filesystem::exists (mCfgMgr.getCachePath())) boost::filesystem::create_directories (mCfgMgr.getCachePath()); @@ -276,7 +316,28 @@ std::auto_ptr CS::Editor::setupGraphics() std::auto_ptr factory (new sh::Factory (platform)); - factory->setCurrentLanguage (sh::Language_GLSL); /// \todo make this configurable + QString shLang = mUserSettings.settingValue("General/shader mode"); + QString rend = renderSystem.c_str(); + bool openGL = rend.contains(QRegExp("^OpenGL", Qt::CaseInsensitive)); + bool glES = rend.contains(QRegExp("^OpenGL ES", Qt::CaseInsensitive)); + + // force shader language based on render system + if(shLang == "" + || (openGL && shLang == "hlsl") + || (!openGL && shLang == "glsl") + || (glES && shLang != "glsles")) + { + shLang = openGL ? (glES ? "glsles" : "glsl") : "hlsl"; + //no group means "General" group in the "ini" file standard + mUserSettings.setDefinitions("shader mode", (QStringList() << shLang)); + } + enum sh::Language lang; + if(shLang == "glsl") lang = sh::Language_GLSL; + else if(shLang == "glsles") lang = sh::Language_GLSLES; + else if(shLang == "hlsl") lang = sh::Language_HLSL; + else lang = sh::Language_CG; + + factory->setCurrentLanguage (lang); factory->setWriteSourceCache (true); factory->setReadSourceCache (true); factory->setReadMicrocodeCache (true); @@ -284,16 +345,27 @@ std::auto_ptr CS::Editor::setupGraphics() factory->loadAllFiles(); - sh::Factory::getInstance().setGlobalSetting ("fog", "true"); + bool shaders = mUserSettings.setting("3d-render/shaders", QString("true")) == "true" ? true : false; + sh::Factory::getInstance ().setShadersEnabled (shaders); + + std::string fog = mUserSettings.setting("Shader/fog", QString("true")).toStdString(); + sh::Factory::getInstance().setGlobalSetting ("fog", fog); + + + std::string shadows = mUserSettings.setting("Shader/shadows", QString("false")).toStdString(); + sh::Factory::getInstance().setGlobalSetting ("shadows", shadows); - sh::Factory::getInstance().setGlobalSetting ("shadows", "false"); - sh::Factory::getInstance().setGlobalSetting ("shadows_pssm", "false"); + std::string shadows_pssm = mUserSettings.setting("Shader/shadows_pssm", QString("false")).toStdString(); + sh::Factory::getInstance().setGlobalSetting ("shadows_pssm", shadows_pssm); - sh::Factory::getInstance ().setGlobalSetting ("render_refraction", "false"); + std::string render_refraction = mUserSettings.setting("Shader/render_refraction", QString("false")).toStdString(); + sh::Factory::getInstance ().setGlobalSetting ("render_refraction", render_refraction); + // internal setting - may be switched on or off by the use of shader configurations sh::Factory::getInstance ().setGlobalSetting ("viewproj_fix", "false"); - sh::Factory::getInstance ().setGlobalSetting ("num_lights", "8"); + std::string num_lights = mUserSettings.setting("3d-render-adv/num_lights", QString("8")).toStdString(); + sh::Factory::getInstance ().setGlobalSetting ("num_lights", num_lights); /// \todo add more configurable shiny settings diff --git a/apps/opencs/editor.hpp b/apps/opencs/editor.hpp index d88da9865..d55b0e873 100644 --- a/apps/opencs/editor.hpp +++ b/apps/opencs/editor.hpp @@ -16,6 +16,8 @@ #include +#include + #include "model/settings/usersettings.hpp" #include "model/doc/documentmanager.hpp" @@ -25,6 +27,8 @@ #include "view/doc/newgame.hpp" #include "view/settings/dialog.hpp" +#include "view/render/overlaysystem.hpp" +#include "view/world/physicsmanager.hpp" namespace OgreInit { @@ -37,8 +41,11 @@ namespace CS { Q_OBJECT + Nif::Cache mNifCache; Files::ConfigurationManager mCfgMgr; CSMSettings::UserSettings mUserSettings; + std::auto_ptr mOverlaySystem; + std::auto_ptr mPhysicsManager; CSMDoc::DocumentManager mDocumentManager; CSVDoc::ViewManager mViewManager; CSVDoc::StartupDialogue mStartup; @@ -61,6 +68,7 @@ namespace CS public: Editor (OgreInit::OgreInit& ogreInit); + ~Editor (); bool makeIPCServer(); void connectToIPCServer(); diff --git a/apps/opencs/model/doc/blacklist.cpp b/apps/opencs/model/doc/blacklist.cpp new file mode 100644 index 000000000..9b37a4302 --- /dev/null +++ b/apps/opencs/model/doc/blacklist.cpp @@ -0,0 +1,31 @@ + +#include "blacklist.hpp" + +#include + +#include + +bool CSMDoc::Blacklist::isBlacklisted (const CSMWorld::UniversalId& id) const +{ + std::map >::const_iterator iter = + mIds.find (id.getType()); + + if (iter==mIds.end()) + return false; + + return std::binary_search (iter->second.begin(), iter->second.end(), + Misc::StringUtils::lowerCase (id.getId())); +} + +void CSMDoc::Blacklist::add (CSMWorld::UniversalId::Type type, + const std::vector& ids) +{ + std::vector& list = mIds[type]; + + int size = list.size(); + + list.resize (size+ids.size()); + + std::transform (ids.begin(), ids.end(), list.begin()+size, Misc::StringUtils::lowerCase); + std::sort (list.begin(), list.end()); +} \ No newline at end of file diff --git a/apps/opencs/model/doc/blacklist.hpp b/apps/opencs/model/doc/blacklist.hpp new file mode 100644 index 000000000..9bf7f1d86 --- /dev/null +++ b/apps/opencs/model/doc/blacklist.hpp @@ -0,0 +1,25 @@ +#ifndef CSM_DOC_BLACKLIST_H +#define CSM_DOC_BLACKLIST_H + +#include +#include +#include + +#include "../world/universalid.hpp" + +namespace CSMDoc +{ + /// \brief ID blacklist sorted by UniversalId type + class Blacklist + { + std::map > mIds; + + public: + + bool isBlacklisted (const CSMWorld::UniversalId& id) const; + + void add (CSMWorld::UniversalId::Type type, const std::vector& ids); + }; +} + +#endif diff --git a/apps/opencs/model/doc/document.cpp b/apps/opencs/model/doc/document.cpp index f452008ac..4fdfd2e5e 100644 --- a/apps/opencs/model/doc/document.cpp +++ b/apps/opencs/model/doc/document.cpp @@ -1,6 +1,7 @@ #include "document.hpp" #include +#include #include @@ -2022,6 +2023,7 @@ void CSMDoc::Document::addOptionalGmsts() { ESM::GameSetting gmst; gmst.mId = sFloats[i]; + gmst.blank(); gmst.mValue.setType (ESM::VT_Float); addOptionalGmst (gmst); } @@ -2030,6 +2032,7 @@ void CSMDoc::Document::addOptionalGmsts() { ESM::GameSetting gmst; gmst.mId = sIntegers[i]; + gmst.blank(); gmst.mValue.setType (ESM::VT_Int); addOptionalGmst (gmst); } @@ -2038,6 +2041,7 @@ void CSMDoc::Document::addOptionalGmsts() { ESM::GameSetting gmst; gmst.mId = sStrings[i]; + gmst.blank(); gmst.mValue.setType (ESM::VT_String); gmst.mValue.setString (""); addOptionalGmst (gmst); @@ -2058,6 +2062,7 @@ void CSMDoc::Document::addOptionalGlobals() { ESM::Global global; global.mId = sGlobals[i]; + global.blank(); global.mValue.setType (ESM::VT_Long); if (i==0) @@ -2067,6 +2072,19 @@ void CSMDoc::Document::addOptionalGlobals() } } +void CSMDoc::Document::addOptionalMagicEffects() +{ + for (int i=ESM::MagicEffect::SummonFabricant; i<=ESM::MagicEffect::SummonCreature05; ++i) + { + ESM::MagicEffect effect; + effect.mIndex = i; + effect.mId = ESM::MagicEffect::indexToId (i); + effect.blank(); + + addOptionalMagicEffect (effect); + } +} + void CSMDoc::Document::addOptionalGmst (const ESM::GameSetting& gmst) { if (getData().getGmsts().searchId (gmst.mId)==-1) @@ -2089,6 +2107,17 @@ void CSMDoc::Document::addOptionalGlobal (const ESM::Global& global) } } +void CSMDoc::Document::addOptionalMagicEffect (const ESM::MagicEffect& magicEffect) +{ + if (getData().getMagicEffects().searchId (magicEffect.mId)==-1) + { + CSMWorld::Record record; + record.mBase = magicEffect; + record.mState = CSMWorld::RecordBase::State_BaseOnly; + getData().getMagicEffects().appendRecord (record); + } +} + void CSMDoc::Document::createBase() { static const char *sGlobals[] = @@ -2200,33 +2229,49 @@ void CSMDoc::Document::createBase() getData().getTopics().add (record); } + + for (int i=0; i& files, bool new_, const boost::filesystem::path& savePath, const boost::filesystem::path& resDir, - ToUTF8::FromType encoding) -: mSavePath (savePath), mContentFiles (files), mNew (new_), mData (encoding), mTools (mData), - mResDir(resDir), + ToUTF8::FromType encoding, const CSMWorld::ResourcesManager& resourcesManager, + const std::vector& blacklistedScripts) +: mSavePath (savePath), mContentFiles (files), mNew (new_), mData (encoding, resourcesManager), + mTools (*this), mResDir(resDir), mProjectPath ((configuration.getUserDataPath() / "projects") / (savePath.filename().string() + ".project")), - mSaving (*this, mProjectPath, encoding) + mSaving (*this, mProjectPath, encoding), + mRunner (mProjectPath) { if (mContentFiles.empty()) throw std::runtime_error ("Empty content file sequence"); if (!boost::filesystem::exists (mProjectPath)) { - boost::filesystem::path locCustomFiltersPath (configuration.getUserDataPath()); - locCustomFiltersPath /= "defaultfilters"; + boost::filesystem::path customFiltersPath (configuration.getUserDataPath()); + customFiltersPath /= "defaultfilters"; + + std::ofstream destination (mProjectPath.string().c_str(), std::ios::binary); - if (boost::filesystem::exists (locCustomFiltersPath)) + if (boost::filesystem::exists (customFiltersPath)) { - boost::filesystem::copy_file (locCustomFiltersPath, mProjectPath); + destination << std::ifstream(customFiltersPath.c_str(), std::ios::binary).rdbuf(); } else { - boost::filesystem::copy_file (mResDir / "defaultfilters", mProjectPath); + destination << std::ifstream(std::string(mResDir.string() + "/defaultfilters").c_str(), std::ios::binary).rdbuf(); } } @@ -2239,20 +2284,25 @@ CSMDoc::Document::Document (const Files::ConfigurationManager& configuration, createBase(); } + mBlacklist.add (CSMWorld::UniversalId::Type_Script, blacklistedScripts); + addOptionalGmsts(); addOptionalGlobals(); + addOptionalMagicEffects(); connect (&mUndoStack, SIGNAL (cleanChanged (bool)), this, SLOT (modificationStateChanged (bool))); connect (&mTools, SIGNAL (progress (int, int, int)), this, SLOT (progress (int, int, int))); - connect (&mTools, SIGNAL (done (int)), this, SLOT (operationDone (int))); + connect (&mTools, SIGNAL (done (int, bool)), this, SLOT (operationDone (int, bool))); connect (&mSaving, SIGNAL (progress (int, int, int)), this, SLOT (progress (int, int, int))); - connect (&mSaving, SIGNAL (done (int)), this, SLOT (operationDone (int))); + connect (&mSaving, SIGNAL (done (int, bool)), this, SLOT (operationDone (int, bool))); connect ( &mSaving, SIGNAL (reportMessage (const CSMWorld::UniversalId&, const std::string&, int)), this, SLOT (reportMessage (const CSMWorld::UniversalId&, const std::string&, int))); + + connect (&mRunner, SIGNAL (runStateChanged()), this, SLOT (runStateChanged())); } CSMDoc::Document::~Document() @@ -2274,6 +2324,9 @@ int CSMDoc::Document::getState() const if (mSaving.isRunning()) state |= State_Locked | State_Saving | State_Operation; + if (mRunner.isRunning()) + state |= State_Locked | State_Running; + if (int operations = mTools.getRunningOperations()) state |= State_Locked | State_Operation | operations; @@ -2338,7 +2391,7 @@ void CSMDoc::Document::reportMessage (const CSMWorld::UniversalId& id, const std std::cout << message << std::endl; } -void CSMDoc::Document::operationDone (int type) +void CSMDoc::Document::operationDone (int type, bool failed) { emit stateChanged (getState(), this); } @@ -2358,6 +2411,55 @@ CSMTools::ReportModel *CSMDoc::Document::getReport (const CSMWorld::UniversalId& return mTools.getReport (id); } +bool CSMDoc::Document::isBlacklisted (const CSMWorld::UniversalId& id) + const +{ + return mBlacklist.isBlacklisted (id); +} + +void CSMDoc::Document::startRunning (const std::string& profile, + const std::string& startupInstruction) +{ + std::vector contentFiles; + + for (std::vector::const_iterator iter (mContentFiles.begin()); + iter!=mContentFiles.end(); ++iter) + contentFiles.push_back (iter->filename().string()); + + mRunner.configure (getData().getDebugProfiles().getRecord (profile).get(), contentFiles, + startupInstruction); + + int state = getState(); + + if (state & State_Modified) + { + // need to save first + mRunner.start (true); + + new SaveWatcher (&mRunner, &mSaving); // no, that is not a memory leak. Qt is weird. + + if (!(state & State_Saving)) + save(); + } + else + mRunner.start(); +} + +void CSMDoc::Document::stopRunning() +{ + mRunner.stop(); +} + +QTextDocument *CSMDoc::Document::getRunLog() +{ + return mRunner.getLog(); +} + +void CSMDoc::Document::runStateChanged() +{ + emit stateChanged (getState(), this); +} + void CSMDoc::Document::progress (int current, int max, int type) { emit progress (current, max, type, 1, this); diff --git a/apps/opencs/model/doc/document.hpp b/apps/opencs/model/doc/document.hpp index a6f8aaae2..c5f6d1006 100644 --- a/apps/opencs/model/doc/document.hpp +++ b/apps/opencs/model/doc/document.hpp @@ -17,6 +17,8 @@ #include "state.hpp" #include "saving.hpp" +#include "blacklist.hpp" +#include "runner.hpp" class QAbstractItemModel; @@ -24,6 +26,7 @@ namespace ESM { struct GameSetting; struct Global; + struct MagicEffect; } namespace Files @@ -31,6 +34,11 @@ namespace Files class ConfigurationManager; } +namespace CSMWorld +{ + class ResourcesManager; +} + namespace CSMDoc { class Document : public QObject @@ -47,6 +55,8 @@ namespace CSMDoc boost::filesystem::path mProjectPath; Saving mSaving; boost::filesystem::path mResDir; + Blacklist mBlacklist; + Runner mRunner; // It is important that the undo stack is declared last, because on desctruction it fires a signal, that is connected to a slot, that is // using other member variables. Unfortunately this connection is cut only in the QObject destructor, which is way too late. @@ -64,16 +74,21 @@ namespace CSMDoc void addOptionalGlobals(); + void addOptionalMagicEffects(); + void addOptionalGmst (const ESM::GameSetting& gmst); void addOptionalGlobal (const ESM::Global& global); + void addOptionalMagicEffect (const ESM::MagicEffect& effect); + public: Document (const Files::ConfigurationManager& configuration, const std::vector< boost::filesystem::path >& files, bool new_, const boost::filesystem::path& savePath, const boost::filesystem::path& resDir, - ToUTF8::FromType encoding); + ToUTF8::FromType encoding, const CSMWorld::ResourcesManager& resourcesManager, + const std::vector& blacklistedScripts); ~Document(); @@ -105,6 +120,15 @@ namespace CSMDoc CSMTools::ReportModel *getReport (const CSMWorld::UniversalId& id); ///< The ownership of the returned report is not transferred. + bool isBlacklisted (const CSMWorld::UniversalId& id) const; + + void startRunning (const std::string& profile, + const std::string& startupInstruction = ""); + + void stopRunning(); + + QTextDocument *getRunLog(); + signals: void stateChanged (int state, CSMDoc::Document *document); @@ -118,7 +142,9 @@ namespace CSMDoc void reportMessage (const CSMWorld::UniversalId& id, const std::string& message, int type); - void operationDone (int type); + void operationDone (int type, bool failed); + + void runStateChanged(); public slots: diff --git a/apps/opencs/model/doc/documentmanager.cpp b/apps/opencs/model/doc/documentmanager.cpp index 096864b77..9b807225c 100644 --- a/apps/opencs/model/doc/documentmanager.cpp +++ b/apps/opencs/model/doc/documentmanager.cpp @@ -31,8 +31,8 @@ CSMDoc::DocumentManager::DocumentManager (const Files::ConfigurationManager& con &mLoader, SLOT (loadDocument (CSMDoc::Document *))); connect (&mLoader, SIGNAL (nextStage (CSMDoc::Document *, const std::string&, int)), this, SIGNAL (nextStage (CSMDoc::Document *, const std::string&, int))); - connect (&mLoader, SIGNAL (nextRecord (CSMDoc::Document *)), - this, SIGNAL (nextRecord (CSMDoc::Document *))); + connect (&mLoader, SIGNAL (nextRecord (CSMDoc::Document *, int)), + this, SIGNAL (nextRecord (CSMDoc::Document *, int))); connect (this, SIGNAL (cancelLoading (CSMDoc::Document *)), &mLoader, SLOT (abortLoading (CSMDoc::Document *))); connect (&mLoader, SIGNAL (loadMessage (CSMDoc::Document *, const std::string&)), @@ -52,7 +52,7 @@ CSMDoc::DocumentManager::~DocumentManager() void CSMDoc::DocumentManager::addDocument (const std::vector& files, const boost::filesystem::path& savePath, bool new_) { - Document *document = new Document (mConfiguration, files, new_, savePath, mResDir, mEncoding); + Document *document = new Document (mConfiguration, files, new_, savePath, mResDir, mEncoding, mResourcesManager, mBlacklistedScripts); mDocuments.push_back (document); @@ -85,6 +85,16 @@ void CSMDoc::DocumentManager::setEncoding (ToUTF8::FromType encoding) mEncoding = encoding; } +void CSMDoc::DocumentManager::setBlacklistedScripts (const std::vector& scriptIds) +{ + mBlacklistedScripts = scriptIds; +} + +void CSMDoc::DocumentManager::listResources() +{ + mResourcesManager.listResources(); +} + void CSMDoc::DocumentManager::documentLoaded (Document *document) { emit documentAdded (document); diff --git a/apps/opencs/model/doc/documentmanager.hpp b/apps/opencs/model/doc/documentmanager.hpp index 9b675826a..c545b9a9f 100644 --- a/apps/opencs/model/doc/documentmanager.hpp +++ b/apps/opencs/model/doc/documentmanager.hpp @@ -11,6 +11,8 @@ #include +#include "../world/resourcesmanager.hpp" + #include "loader.hpp" namespace Files @@ -31,6 +33,8 @@ namespace CSMDoc QThread mLoaderThread; Loader mLoader; ToUTF8::FromType mEncoding; + CSMWorld::ResourcesManager mResourcesManager; + std::vector mBlacklistedScripts; DocumentManager (const DocumentManager&); DocumentManager& operator= (const DocumentManager&); @@ -50,6 +54,11 @@ namespace CSMDoc void setEncoding (ToUTF8::FromType encoding); + void setBlacklistedScripts (const std::vector& scriptIds); + + /// Ask OGRE for a list of available resources. + void listResources(); + private: boost::filesystem::path mResDir; @@ -79,9 +88,10 @@ namespace CSMDoc void loadingStopped (CSMDoc::Document *document, bool completed, const std::string& error); - void nextStage (CSMDoc::Document *document, const std::string& name, int steps); + void nextStage (CSMDoc::Document *document, const std::string& name, + int totalRecords); - void nextRecord (CSMDoc::Document *document); + void nextRecord (CSMDoc::Document *document, int records); void cancelLoading (CSMDoc::Document *document); diff --git a/apps/opencs/model/doc/loader.cpp b/apps/opencs/model/doc/loader.cpp index c106c06e8..712deb9df 100644 --- a/apps/opencs/model/doc/loader.cpp +++ b/apps/opencs/model/doc/loader.cpp @@ -8,7 +8,7 @@ #include "document.hpp" #include "state.hpp" -CSMDoc::Loader::Stage::Stage() : mFile (0), mRecordsLeft (false) {} +CSMDoc::Loader::Stage::Stage() : mFile (0), mRecordsLoaded (0), mRecordsLeft (false) {} CSMDoc::Loader::Loader() @@ -39,13 +39,14 @@ void CSMDoc::Loader::load() Document *document = iter->first; int size = static_cast (document->getContentFiles().size()); + int editedIndex = size-1; // index of the file to be edited/created if (document->isNew()) --size; bool done = false; - const int batchingSize = 100; + const int batchingSize = 50; try { @@ -58,17 +59,21 @@ void CSMDoc::Loader::load() iter->second.mRecordsLeft = false; break; } + else + ++(iter->second.mRecordsLoaded); CSMWorld::UniversalId log (CSMWorld::UniversalId::Type_LoadErrorLog, 0); + { // silence a g++ warning for (CSMDoc::Stage::Messages::const_iterator iter (messages.begin()); iter!=messages.end(); ++iter) { document->getReport (log)->add (iter->first, iter->second); emit loadMessage (document, iter->second); } + } - emit nextRecord (document); + emit nextRecord (document, iter->second.mRecordsLoaded); return; } @@ -77,17 +82,19 @@ void CSMDoc::Loader::load() { boost::filesystem::path path = document->getContentFiles()[iter->second.mFile]; - int steps = document->getData().startLoading (path, iter->second.mFilegetData().startLoading (path, iter->second.mFile!=editedIndex, false); iter->second.mRecordsLeft = true; + iter->second.mRecordsLoaded = 0; - emit nextStage (document, path.filename().string(), steps/batchingSize); + emit nextStage (document, path.filename().string(), steps); } else if (iter->second.mFile==size) { int steps = document->getData().startLoading (document->getProjectPath(), false, true); iter->second.mRecordsLeft = true; + iter->second.mRecordsLoaded = 0; - emit nextStage (document, "Project File", steps/batchingSize); + emit nextStage (document, "Project File", steps); } else { diff --git a/apps/opencs/model/doc/loader.hpp b/apps/opencs/model/doc/loader.hpp index c276e14ff..d1ee38f9f 100644 --- a/apps/opencs/model/doc/loader.hpp +++ b/apps/opencs/model/doc/loader.hpp @@ -18,6 +18,7 @@ namespace CSMDoc struct Stage { int mFile; + int mRecordsLoaded; bool mRecordsLeft; Stage(); @@ -56,9 +57,10 @@ namespace CSMDoc ///< Document load has been interrupted either because of a call to abortLoading /// or a problem during loading). In the former case error will be an empty string. - void nextStage (CSMDoc::Document *document, const std::string& name, int steps); + void nextStage (CSMDoc::Document *document, const std::string& name, + int totalRecords); - void nextRecord (CSMDoc::Document *document); + void nextRecord (CSMDoc::Document *document, int records); ///< \note This signal is only given once per group of records. The group size is /// approximately the total number of records divided by the steps value of the /// previous nextStage signal. diff --git a/apps/opencs/model/doc/operation.cpp b/apps/opencs/model/doc/operation.cpp index 42a432043..7f77e8ac9 100644 --- a/apps/opencs/model/doc/operation.cpp +++ b/apps/opencs/model/doc/operation.cpp @@ -27,7 +27,9 @@ void CSMDoc::Operation::prepareStages() } CSMDoc::Operation::Operation (int type, bool ordered, bool finalAlways) -: mType (type), mOrdered (ordered), mFinalAlways (finalAlways) +: mType (type), mStages(std::vector >()), mCurrentStage(mStages.begin()), + mCurrentStep(0), mCurrentStepTotal(0), mTotalSteps(0), mOrdered (ordered), + mFinalAlways (finalAlways), mError(false) { connect (this, SIGNAL (finished()), this, SLOT (operationDone())); } @@ -119,5 +121,5 @@ void CSMDoc::Operation::executeStage() void CSMDoc::Operation::operationDone() { - emit done (mType); -} \ No newline at end of file + emit done (mType, mError); +} diff --git a/apps/opencs/model/doc/operation.hpp b/apps/opencs/model/doc/operation.hpp index 651283880..d5a7d4e09 100644 --- a/apps/opencs/model/doc/operation.hpp +++ b/apps/opencs/model/doc/operation.hpp @@ -54,7 +54,7 @@ namespace CSMDoc void reportMessage (const CSMWorld::UniversalId& id, const std::string& message, int type); - void done (int type); + void done (int type, bool failed); public slots: diff --git a/apps/opencs/model/doc/runner.cpp b/apps/opencs/model/doc/runner.cpp new file mode 100644 index 000000000..d679c1890 --- /dev/null +++ b/apps/opencs/model/doc/runner.cpp @@ -0,0 +1,162 @@ + +#include "runner.hpp" + +#include +#include +#include +#include + +#include "operation.hpp" + +CSMDoc::Runner::Runner (const boost::filesystem::path& projectPath) +: mRunning (false), mStartup (0), mProjectPath (projectPath) +{ + connect (&mProcess, SIGNAL (finished (int, QProcess::ExitStatus)), + this, SLOT (finished (int, QProcess::ExitStatus))); + + connect (&mProcess, SIGNAL (readyReadStandardOutput()), + this, SLOT (readyReadStandardOutput())); + + mProcess.setProcessChannelMode (QProcess::MergedChannels); + + mProfile.blank(); +} + +CSMDoc::Runner::~Runner() +{ + if (mRunning) + { + disconnect (&mProcess, 0, this, 0); + mProcess.kill(); + mProcess.waitForFinished(); + } +} + +void CSMDoc::Runner::start (bool delayed) +{ + if (mStartup) + { + delete mStartup; + mStartup = 0; + } + + if (!delayed) + { + mLog.clear(); + + QString path = "openmw"; +#ifdef Q_OS_WIN + path.append(QString(".exe")); +#elif defined(Q_OS_MAC) + QDir dir(QCoreApplication::applicationDirPath()); + dir.cdUp(); + dir.cdUp(); + dir.cdUp(); + path = dir.absoluteFilePath(path.prepend("OpenMW.app/Contents/MacOS/")); +#else + path.prepend(QString("./")); +#endif + + mStartup = new QTemporaryFile (this); + mStartup->open(); + + { + QTextStream stream (mStartup); + + if (!mStartupInstruction.empty()) + stream << QString::fromUtf8 (mStartupInstruction.c_str()) << '\n'; + + stream << QString::fromUtf8 (mProfile.mScriptText.c_str()); + } + + mStartup->close(); + + QStringList arguments; + arguments << "--skip-menu"; + + if (mProfile.mFlags & ESM::DebugProfile::Flag_BypassNewGame) + arguments << "--new-game=0"; + else + arguments << "--new-game=1"; + + arguments << ("--script-run="+mStartup->fileName());; + + arguments << + QString::fromUtf8 (("--data="+mProjectPath.parent_path().string()).c_str()); + + for (std::vector::const_iterator iter (mContentFiles.begin()); + iter!=mContentFiles.end(); ++iter) + { + arguments << QString::fromUtf8 (("--content="+*iter).c_str()); + } + + arguments + << QString::fromUtf8 (("--content="+mProjectPath.filename().string()).c_str()); + + mProcess.start (path, arguments); + } + + mRunning = true; + emit runStateChanged(); +} + +void CSMDoc::Runner::stop() +{ + delete mStartup; + mStartup = 0; + + if (mProcess.state()==QProcess::NotRunning) + { + mRunning = false; + emit runStateChanged(); + } + else + mProcess.kill(); +} + +bool CSMDoc::Runner::isRunning() const +{ + return mRunning; +} + +void CSMDoc::Runner::configure (const ESM::DebugProfile& profile, + const std::vector& contentFiles, const std::string& startupInstruction) +{ + mProfile = profile; + mContentFiles = contentFiles; + mStartupInstruction = startupInstruction; +} + +void CSMDoc::Runner::finished (int exitCode, QProcess::ExitStatus exitStatus) +{ + mRunning = false; + emit runStateChanged(); +} + +QTextDocument *CSMDoc::Runner::getLog() +{ + return &mLog; +} + +void CSMDoc::Runner::readyReadStandardOutput() +{ + mLog.setPlainText ( + mLog.toPlainText() + QString::fromUtf8 (mProcess.readAllStandardOutput())); +} + + +CSMDoc::SaveWatcher::SaveWatcher (Runner *runner, Operation *operation) +: QObject (runner), mRunner (runner) +{ + connect (operation, SIGNAL (done (int, bool)), this, SLOT (saveDone (int, bool))); +} + +void CSMDoc::SaveWatcher::saveDone (int type, bool failed) +{ + if (failed) + mRunner->stop(); + else + mRunner->start(); + + deleteLater(); +} diff --git a/apps/opencs/model/doc/runner.hpp b/apps/opencs/model/doc/runner.hpp new file mode 100644 index 000000000..38b52a73b --- /dev/null +++ b/apps/opencs/model/doc/runner.hpp @@ -0,0 +1,85 @@ +#ifndef CSM_DOC_RUNNER_H +#define CSM_DOC_RUNNER_H + +#include +#include + +#include + +#include +#include +#include + +#include + +class QTemporaryFile; + +namespace CSMDoc +{ + class Runner : public QObject + { + Q_OBJECT + + QProcess mProcess; + bool mRunning; + ESM::DebugProfile mProfile; + std::vector mContentFiles; + std::string mStartupInstruction; + QTemporaryFile *mStartup; + QTextDocument mLog; + boost::filesystem::path mProjectPath; + + public: + + Runner (const boost::filesystem::path& projectPath); + + ~Runner(); + + /// \param delayed Flag as running but do not start the OpenMW process yet (the + /// process must be started by another call of start with delayed==false) + void start (bool delayed = false); + + void stop(); + + /// \note Running state is entered when the start function is called. This + /// is not necessarily identical to the moment the child process is started. + bool isRunning() const; + + void configure (const ESM::DebugProfile& profile, + const std::vector& contentFiles, + const std::string& startupInstruction); + + QTextDocument *getLog(); + + signals: + + void runStateChanged(); + + private slots: + + void finished (int exitCode, QProcess::ExitStatus exitStatus); + + void readyReadStandardOutput(); + }; + + class Operation; + + /// \brief Watch for end of save operation and restart or stop runner + class SaveWatcher : public QObject + { + Q_OBJECT + + Runner *mRunner; + + public: + + /// *this attaches itself to runner + SaveWatcher (Runner *runner, Operation *operation); + + private slots: + + void saveDone (int type, bool failed); + }; +} + +#endif diff --git a/apps/opencs/model/doc/saving.cpp b/apps/opencs/model/doc/saving.cpp index 27d21635e..70e9e1d87 100644 --- a/apps/opencs/model/doc/saving.cpp +++ b/apps/opencs/model/doc/saving.cpp @@ -17,7 +17,14 @@ CSMDoc::Saving::Saving (Document& document, const boost::filesystem::path& proje appendStage (new WriteHeaderStage (mDocument, mState, true)); - appendStage (new WriteFilterStage (mDocument, mState, CSMFilter::Filter::Scope_Project)); + appendStage (new WriteCollectionStage > ( + mDocument.getData().getFilters(), mState, CSMWorld::Scope_Project)); + + appendStage (new WriteCollectionStage > ( + mDocument.getData().getDebugProfiles(), mState, CSMWorld::Scope_Project)); + + appendStage (new WriteCollectionStage > ( + mDocument.getData().getScripts(), mState, CSMWorld::Scope_Project)); appendStage (new CloseSaveStage (mState)); @@ -59,13 +66,31 @@ CSMDoc::Saving::Saving (Document& document, const boost::filesystem::path& proje appendStage (new WriteCollectionStage > (mDocument.getData().getSpells(), mState)); + appendStage (new WriteCollectionStage > + (mDocument.getData().getEnchantments(), mState)); + + appendStage (new WriteCollectionStage > + (mDocument.getData().getBodyParts(), mState)); + + appendStage (new WriteCollectionStage > + (mDocument.getData().getSoundGens(), mState)); + + appendStage (new WriteCollectionStage > + (mDocument.getData().getMagicEffects(), mState)); + appendStage (new WriteDialogueCollectionStage (mDocument, mState, false)); appendStage (new WriteDialogueCollectionStage (mDocument, mState, true)); appendStage (new WriteRefIdCollectionStage (mDocument, mState)); + appendStage (new CollectionReferencesStage (mDocument, mState)); + + appendStage (new WriteCellCollectionStage (mDocument, mState)); + + appendStage (new WritePathgridCollectionStage (mDocument, mState)); + // close file and clean up appendStage (new CloseSaveStage (mState)); appendStage (new FinalSavingStage (mDocument, mState)); diff --git a/apps/opencs/model/doc/savingstages.cpp b/apps/opencs/model/doc/savingstages.cpp index 066c76734..08f8c9eaa 100644 --- a/apps/opencs/model/doc/savingstages.cpp +++ b/apps/opencs/model/doc/savingstages.cpp @@ -9,6 +9,8 @@ #include +#include + #include "../world/infocollection.hpp" #include "document.hpp" @@ -199,20 +201,155 @@ void CSMDoc::WriteRefIdCollectionStage::perform (int stage, Messages& messages) } -CSMDoc::WriteFilterStage::WriteFilterStage (Document& document, SavingState& state, - CSMFilter::Filter::Scope scope) -: WriteCollectionStage > (document.getData().getFilters(), - state), - mDocument (document), mScope (scope) +CSMDoc::CollectionReferencesStage::CollectionReferencesStage (Document& document, + SavingState& state) +: mDocument (document), mState (state) +{} + +int CSMDoc::CollectionReferencesStage::setup() +{ + mState.getSubRecords().clear(); + + int size = mDocument.getData().getReferences().getSize(); + + int steps = size/100; + if (size%100) ++steps; + + return steps; +} + +void CSMDoc::CollectionReferencesStage::perform (int stage, Messages& messages) +{ + int size = mDocument.getData().getReferences().getSize(); + + for (int i=stage*100; i& record = + mDocument.getData().getReferences().getRecord (i); + + if (record.mState==CSMWorld::RecordBase::State_Deleted || + record.mState==CSMWorld::RecordBase::State_Modified || + record.mState==CSMWorld::RecordBase::State_ModifiedOnly) + { + mState.getSubRecords()[Misc::StringUtils::lowerCase (record.get().mCell)] + .push_back (i); + } + } +} + + +CSMDoc::WriteCellCollectionStage::WriteCellCollectionStage (Document& document, + SavingState& state) +: mDocument (document), mState (state) +{} + +int CSMDoc::WriteCellCollectionStage::setup() +{ + return mDocument.getData().getCells().getSize(); +} + +void CSMDoc::WriteCellCollectionStage::perform (int stage, Messages& messages) +{ + const CSMWorld::Record& cell = + mDocument.getData().getCells().getRecord (stage); + + std::map >::const_iterator references = + mState.getSubRecords().find (Misc::StringUtils::lowerCase (cell.get().mId)); + + if (cell.mState==CSMWorld::RecordBase::State_Modified || + cell.mState==CSMWorld::RecordBase::State_ModifiedOnly || + references!=mState.getSubRecords().end()) + { + bool interior = cell.get().mId.substr (0, 1)!="#"; + + // write cell data + mState.getWriter().startRecord (cell.mModified.sRecordId); + + mState.getWriter().writeHNOCString ("NAME", cell.get().mName); + + ESM::Cell cell2 = cell.get(); + + if (interior) + cell2.mData.mFlags |= ESM::Cell::Interior; + else + { + cell2.mData.mFlags &= ~ESM::Cell::Interior; + + std::istringstream stream (cell.get().mId.c_str()); + char ignore; + stream >> ignore >> cell2.mData.mX >> cell2.mData.mY; + } + cell2.save (mState.getWriter()); + + // write references + if (references!=mState.getSubRecords().end()) + { + for (std::vector::const_iterator iter (references->second.begin()); + iter!=references->second.end(); ++iter) + { + const CSMWorld::Record& ref = + mDocument.getData().getReferences().getRecord (*iter); + + if (ref.mState==CSMWorld::RecordBase::State_Modified || + ref.mState==CSMWorld::RecordBase::State_ModifiedOnly) + { + ref.get().save (mState.getWriter()); + } + else if (ref.mState==CSMWorld::RecordBase::State_Deleted) + { + /// \todo write record with delete flag + } + } + } + + mState.getWriter().endRecord (cell.mModified.sRecordId); + } + else if (cell.mState==CSMWorld::RecordBase::State_Deleted) + { + /// \todo write record with delete flag + } +} + + +CSMDoc::WritePathgridCollectionStage::WritePathgridCollectionStage (Document& document, + SavingState& state) +: mDocument (document), mState (state) {} -void CSMDoc::WriteFilterStage::perform (int stage, Messages& messages) +int CSMDoc::WritePathgridCollectionStage::setup() { - const CSMWorld::Record& record = - mDocument.getData().getFilters().getRecord (stage); + return mDocument.getData().getPathgrids().getSize(); +} + +void CSMDoc::WritePathgridCollectionStage::perform (int stage, Messages& messages) +{ + const CSMWorld::Record& pathgrid = + mDocument.getData().getPathgrids().getRecord (stage); + + if (pathgrid.mState==CSMWorld::RecordBase::State_Modified || + pathgrid.mState==CSMWorld::RecordBase::State_ModifiedOnly) + { + CSMWorld::Pathgrid record = pathgrid.get(); - if (record.get().mScope==mScope) - WriteCollectionStage >::perform (stage, messages); + if (record.mId.substr (0, 1)=="#") + { + std::istringstream stream (record.mId.c_str()); + char ignore; + stream >> ignore >> record.mData.mX >> record.mData.mY; + } + else + record.mCell = record.mId; + + mState.getWriter().startRecord (record.sRecordId); + + record.save (mState.getWriter()); + + mState.getWriter().endRecord (record.sRecordId); + } + else if (pathgrid.mState==CSMWorld::RecordBase::State_Deleted) + { + /// \todo write record with delete flag + } } diff --git a/apps/opencs/model/doc/savingstages.hpp b/apps/opencs/model/doc/savingstages.hpp index c2f0a150a..87c9ba7eb 100644 --- a/apps/opencs/model/doc/savingstages.hpp +++ b/apps/opencs/model/doc/savingstages.hpp @@ -5,8 +5,7 @@ #include "../world/record.hpp" #include "../world/idcollection.hpp" - -#include "../filter/filter.hpp" +#include "../world/scope.hpp" #include "savingstate.hpp" @@ -67,10 +66,12 @@ namespace CSMDoc { const CollectionT& mCollection; SavingState& mState; + CSMWorld::Scope mScope; public: - WriteCollectionStage (const CollectionT& collection, SavingState& state); + WriteCollectionStage (const CollectionT& collection, SavingState& state, + CSMWorld::Scope scope = CSMWorld::Scope_Content); virtual int setup(); ///< \return number of steps @@ -81,8 +82,8 @@ namespace CSMDoc template WriteCollectionStage::WriteCollectionStage (const CollectionT& collection, - SavingState& state) - : mCollection (collection), mState (state) + SavingState& state, CSMWorld::Scope scope) + : mCollection (collection), mState (state), mScope (scope) {} template @@ -94,16 +95,14 @@ namespace CSMDoc template void WriteCollectionStage::perform (int stage, Messages& messages) { + if (CSMWorld::getScopeFromId (mCollection.getRecord (stage).get().mId)!=mScope) + return; + CSMWorld::RecordBase::State state = mCollection.getRecord (stage).mState; if (state==CSMWorld::RecordBase::State_Modified || state==CSMWorld::RecordBase::State_ModifiedOnly) { - std::string type; - for (int i=0; i<4; ++i) - /// \todo make endianess agnostic (change ESMWriter interface?) - type += reinterpret_cast (&mCollection.getRecord (stage).mModified.sRecordId)[i]; - mState.getWriter().startRecord (mCollection.getRecord (stage).mModified.sRecordId); mState.getWriter().writeHNCString ("NAME", mCollection.getId (stage)); mCollection.getRecord (stage).mModified.save (mState.getWriter()); @@ -152,19 +151,54 @@ namespace CSMDoc }; - class WriteFilterStage : public WriteCollectionStage > + class CollectionReferencesStage : public Stage { Document& mDocument; - CSMFilter::Filter::Scope mScope; + SavingState& mState; public: - WriteFilterStage (Document& document, SavingState& state, CSMFilter::Filter::Scope scope); + CollectionReferencesStage (Document& document, SavingState& state); + + virtual int setup(); + ///< \return number of steps virtual void perform (int stage, Messages& messages); ///< Messages resulting from this stage will be appended to \a messages. }; + class WriteCellCollectionStage : public Stage + { + Document& mDocument; + SavingState& mState; + + public: + + WriteCellCollectionStage (Document& document, SavingState& state); + + virtual int setup(); + ///< \return number of steps + + virtual void perform (int stage, Messages& messages); + ///< Messages resulting from this stage will be appended to \a messages. + }; + + + class WritePathgridCollectionStage : public Stage + { + Document& mDocument; + SavingState& mState; + + public: + + WritePathgridCollectionStage (Document& document, SavingState& state); + + virtual int setup(); + ///< \return number of steps + + virtual void perform (int stage, Messages& messages); + ///< Messages resulting from this stage will be appended to \a messages. + }; class CloseSaveStage : public Stage { diff --git a/apps/opencs/model/doc/savingstate.cpp b/apps/opencs/model/doc/savingstate.cpp index 07c42a490..84bca1e95 100644 --- a/apps/opencs/model/doc/savingstate.cpp +++ b/apps/opencs/model/doc/savingstate.cpp @@ -25,6 +25,8 @@ void CSMDoc::SavingState::start (Document& document, bool project) mStream.clear(); + mSubRecords.clear(); + if (project) mPath = mProjectPath; else @@ -61,3 +63,8 @@ bool CSMDoc::SavingState::isProjectFile() const { return mProjectFile; } + +std::map >& CSMDoc::SavingState::getSubRecords() +{ + return mSubRecords; +} diff --git a/apps/opencs/model/doc/savingstate.hpp b/apps/opencs/model/doc/savingstate.hpp index 7b0c37386..577fc734d 100644 --- a/apps/opencs/model/doc/savingstate.hpp +++ b/apps/opencs/model/doc/savingstate.hpp @@ -2,6 +2,7 @@ #define CSM_DOC_SAVINGSTATE_H #include +#include #include #include @@ -25,6 +26,7 @@ namespace CSMDoc ESM::ESMWriter mWriter; boost::filesystem::path mProjectPath; bool mProjectFile; + std::map > mSubRecords; // record ID, list of subrecords public: @@ -46,6 +48,8 @@ namespace CSMDoc bool isProjectFile() const; ///< Currently saving project file? (instead of content file) + + std::map >& getSubRecords(); }; diff --git a/apps/opencs/model/doc/state.hpp b/apps/opencs/model/doc/state.hpp index 6e1a1c4f4..287439a8b 100644 --- a/apps/opencs/model/doc/state.hpp +++ b/apps/opencs/model/doc/state.hpp @@ -8,12 +8,13 @@ namespace CSMDoc State_Modified = 1, State_Locked = 2, State_Operation = 4, + State_Running = 8, - State_Saving = 8, - State_Verifying = 16, - State_Compiling = 32, // not implemented yet - State_Searching = 64, // not implemented yet - State_Loading = 128 // pseudo-state; can not be encountered in a loaded document + State_Saving = 16, + State_Verifying = 32, + State_Compiling = 64, // not implemented yet + State_Searching = 128, // not implemented yet + State_Loading = 256 // pseudo-state; can not be encountered in a loaded document }; } diff --git a/apps/opencs/model/filter/andnode.cpp b/apps/opencs/model/filter/andnode.cpp index dfaa56e78..4249fc228 100644 --- a/apps/opencs/model/filter/andnode.cpp +++ b/apps/opencs/model/filter/andnode.cpp @@ -7,7 +7,7 @@ CSMFilter::AndNode::AndNode (const std::vector >& nodes) : NAryNode (nodes, "and") {} -bool CSMFilter::AndNode::test (const CSMWorld::IdTable& table, int row, +bool CSMFilter::AndNode::test (const CSMWorld::IdTableBase& table, int row, const std::map& columns) const { int size = getSize(); diff --git a/apps/opencs/model/filter/andnode.hpp b/apps/opencs/model/filter/andnode.hpp index 887175700..3838b451d 100644 --- a/apps/opencs/model/filter/andnode.hpp +++ b/apps/opencs/model/filter/andnode.hpp @@ -11,7 +11,7 @@ namespace CSMFilter AndNode (const std::vector >& nodes); - virtual bool test (const CSMWorld::IdTable& table, int row, + virtual bool test (const CSMWorld::IdTableBase& table, int row, const std::map& columns) const; ///< \return Can the specified table row pass through to filter? /// \param columns column ID to column index mapping diff --git a/apps/opencs/model/filter/booleannode.cpp b/apps/opencs/model/filter/booleannode.cpp index 267e06a64..2daa1b6d8 100644 --- a/apps/opencs/model/filter/booleannode.cpp +++ b/apps/opencs/model/filter/booleannode.cpp @@ -3,7 +3,7 @@ CSMFilter::BooleanNode::BooleanNode (bool true_) : mTrue (true_) {} -bool CSMFilter::BooleanNode::test (const CSMWorld::IdTable& table, int row, +bool CSMFilter::BooleanNode::test (const CSMWorld::IdTableBase& table, int row, const std::map& columns) const { return mTrue; diff --git a/apps/opencs/model/filter/booleannode.hpp b/apps/opencs/model/filter/booleannode.hpp index d19219e35..d9635746c 100644 --- a/apps/opencs/model/filter/booleannode.hpp +++ b/apps/opencs/model/filter/booleannode.hpp @@ -13,7 +13,7 @@ namespace CSMFilter BooleanNode (bool true_); - virtual bool test (const CSMWorld::IdTable& table, int row, + virtual bool test (const CSMWorld::IdTableBase& table, int row, const std::map& columns) const; ///< \return Can the specified table row pass through to filter? /// \param columns column ID to column index mapping diff --git a/apps/opencs/model/filter/filter.hpp b/apps/opencs/model/filter/filter.hpp deleted file mode 100644 index 62170ca80..000000000 --- a/apps/opencs/model/filter/filter.hpp +++ /dev/null @@ -1,25 +0,0 @@ -#ifndef CSM_FILTER_FILTER_H -#define CSM_FILTER_FILTER_H - -#include -#include - -#include - -namespace CSMFilter -{ - /// \brief Wrapper for Filter record - struct Filter : public ESM::Filter - { - enum Scope - { - Scope_Project = 0, // per project - Scope_Session = 1, // exists only for one editing session; not saved - Scope_Content = 2 // embedded in the edited content file - }; - - Scope mScope; - }; -} - -#endif \ No newline at end of file diff --git a/apps/opencs/model/filter/node.hpp b/apps/opencs/model/filter/node.hpp index ef18353a4..58588bdc5 100644 --- a/apps/opencs/model/filter/node.hpp +++ b/apps/opencs/model/filter/node.hpp @@ -11,7 +11,7 @@ namespace CSMWorld { - class IdTable; + class IdTableBase; } namespace CSMFilter @@ -32,7 +32,7 @@ namespace CSMFilter virtual ~Node(); - virtual bool test (const CSMWorld::IdTable& table, int row, + virtual bool test (const CSMWorld::IdTableBase& table, int row, const std::map& columns) const = 0; ///< \return Can the specified table row pass through to filter? /// \param columns column ID to column index mapping diff --git a/apps/opencs/model/filter/notnode.cpp b/apps/opencs/model/filter/notnode.cpp index 1b22ea7a6..231773075 100644 --- a/apps/opencs/model/filter/notnode.cpp +++ b/apps/opencs/model/filter/notnode.cpp @@ -3,7 +3,7 @@ CSMFilter::NotNode::NotNode (boost::shared_ptr child) : UnaryNode (child, "not") {} -bool CSMFilter::NotNode::test (const CSMWorld::IdTable& table, int row, +bool CSMFilter::NotNode::test (const CSMWorld::IdTableBase& table, int row, const std::map& columns) const { return !getChild().test (table, row, columns); diff --git a/apps/opencs/model/filter/notnode.hpp b/apps/opencs/model/filter/notnode.hpp index b9e80b8c6..0281d99da 100644 --- a/apps/opencs/model/filter/notnode.hpp +++ b/apps/opencs/model/filter/notnode.hpp @@ -11,7 +11,7 @@ namespace CSMFilter NotNode (boost::shared_ptr child); - virtual bool test (const CSMWorld::IdTable& table, int row, + virtual bool test (const CSMWorld::IdTableBase& table, int row, const std::map& columns) const; ///< \return Can the specified table row pass through to filter? /// \param columns column ID to column index mapping diff --git a/apps/opencs/model/filter/ornode.cpp b/apps/opencs/model/filter/ornode.cpp index 4fc34e1d5..c5d15a384 100644 --- a/apps/opencs/model/filter/ornode.cpp +++ b/apps/opencs/model/filter/ornode.cpp @@ -7,7 +7,7 @@ CSMFilter::OrNode::OrNode (const std::vector >& nodes) : NAryNode (nodes, "or") {} -bool CSMFilter::OrNode::test (const CSMWorld::IdTable& table, int row, +bool CSMFilter::OrNode::test (const CSMWorld::IdTableBase& table, int row, const std::map& columns) const { int size = getSize(); diff --git a/apps/opencs/model/filter/ornode.hpp b/apps/opencs/model/filter/ornode.hpp index c39e35095..07ab51a6a 100644 --- a/apps/opencs/model/filter/ornode.hpp +++ b/apps/opencs/model/filter/ornode.hpp @@ -11,7 +11,7 @@ namespace CSMFilter OrNode (const std::vector >& nodes); - virtual bool test (const CSMWorld::IdTable& table, int row, + virtual bool test (const CSMWorld::IdTableBase& table, int row, const std::map& columns) const; ///< \return Can the specified table row pass through to filter? /// \param columns column ID to column index mapping diff --git a/apps/opencs/model/filter/parser.cpp b/apps/opencs/model/filter/parser.cpp index 6e286d943..51338dfc9 100644 --- a/apps/opencs/model/filter/parser.cpp +++ b/apps/opencs/model/filter/parser.cpp @@ -550,7 +550,12 @@ bool CSMFilter::Parser::parse (const std::string& filter, bool allowPredefined) if (allowPredefined) token = getNextToken(); - if (!allowPredefined || token==Token (Token::Type_OneShot)) + if (allowPredefined && token==Token (Token::Type_EOS)) + { + mFilter.reset(); + return true; + } + else if (!allowPredefined || token==Token (Token::Type_OneShot)) { boost::shared_ptr node = parseImp (true, token!=Token (Token::Type_OneShot)); @@ -591,7 +596,7 @@ bool CSMFilter::Parser::parse (const std::string& filter, bool allowPredefined) return false; } - const CSMWorld::Record& record = mData.getFilters().getRecord (index); + const CSMWorld::Record& record = mData.getFilters().getRecord (index); if (record.isDeleted()) { diff --git a/apps/opencs/model/filter/textnode.cpp b/apps/opencs/model/filter/textnode.cpp index 7d1a4845f..24cdce4f5 100644 --- a/apps/opencs/model/filter/textnode.cpp +++ b/apps/opencs/model/filter/textnode.cpp @@ -7,13 +7,13 @@ #include #include "../world/columns.hpp" -#include "../world/idtable.hpp" +#include "../world/idtablebase.hpp" CSMFilter::TextNode::TextNode (int columnId, const std::string& text) : mColumnId (columnId), mText (text) {} -bool CSMFilter::TextNode::test (const CSMWorld::IdTable& table, int row, +bool CSMFilter::TextNode::test (const CSMWorld::IdTableBase& table, int row, const std::map& columns) const { const std::map::const_iterator iter = columns.find (mColumnId); diff --git a/apps/opencs/model/filter/textnode.hpp b/apps/opencs/model/filter/textnode.hpp index 663fa7382..60ead85de 100644 --- a/apps/opencs/model/filter/textnode.hpp +++ b/apps/opencs/model/filter/textnode.hpp @@ -14,7 +14,7 @@ namespace CSMFilter TextNode (int columnId, const std::string& text); - virtual bool test (const CSMWorld::IdTable& table, int row, + virtual bool test (const CSMWorld::IdTableBase& table, int row, const std::map& columns) const; ///< \return Can the specified table row pass through to filter? /// \param columns column ID to column index mapping diff --git a/apps/opencs/model/filter/valuenode.cpp b/apps/opencs/model/filter/valuenode.cpp index fdcce00ab..66b6282d7 100644 --- a/apps/opencs/model/filter/valuenode.cpp +++ b/apps/opencs/model/filter/valuenode.cpp @@ -5,13 +5,13 @@ #include #include "../world/columns.hpp" -#include "../world/idtable.hpp" +#include "../world/idtablebase.hpp" CSMFilter::ValueNode::ValueNode (int columnId, Type lowerType, Type upperType, double lower, double upper) : mColumnId (columnId), mLowerType (lowerType), mUpperType (upperType), mLower (lower), mUpper (upper){} -bool CSMFilter::ValueNode::test (const CSMWorld::IdTable& table, int row, +bool CSMFilter::ValueNode::test (const CSMWorld::IdTableBase& table, int row, const std::map& columns) const { const std::map::const_iterator iter = columns.find (mColumnId); diff --git a/apps/opencs/model/filter/valuenode.hpp b/apps/opencs/model/filter/valuenode.hpp index b1050709d..5b7ffec4e 100644 --- a/apps/opencs/model/filter/valuenode.hpp +++ b/apps/opencs/model/filter/valuenode.hpp @@ -27,7 +27,7 @@ namespace CSMFilter ValueNode (int columnId, Type lowerType, Type upperType, double lower, double upper); - virtual bool test (const CSMWorld::IdTable& table, int row, + virtual bool test (const CSMWorld::IdTableBase& table, int row, const std::map& columns) const; ///< \return Can the specified table row pass through to filter? /// \param columns column ID to column index mapping diff --git a/apps/opencs/model/settings/setting.cpp b/apps/opencs/model/settings/setting.cpp index 2f86d4ff8..9e33ab916 100644 --- a/apps/opencs/model/settings/setting.cpp +++ b/apps/opencs/model/settings/setting.cpp @@ -2,8 +2,8 @@ #include "support.hpp" CSMSettings::Setting::Setting(SettingType typ, const QString &settingName, - const QString &pageName) - : mIsEditorSetting (false) + const QString &pageName, const QString& label) +: mIsEditorSetting (true) { buildDefaultSetting(); @@ -17,6 +17,7 @@ CSMSettings::Setting::Setting(SettingType typ, const QString &settingName, setProperty (Property_SettingType, QVariant (settingType).toString()); setProperty (Property_Page, pageName); setProperty (Property_Name, settingName); + setProperty (Property_Label, label.isEmpty() ? settingName : label); } void CSMSettings::Setting::buildDefaultSetting() @@ -194,6 +195,16 @@ QString CSMSettings::Setting::page() const return property (Property_Page).at(0); } +void CSMSettings::Setting::setStyleSheet (const QString &value) +{ + setProperty (Property_StyleSheet, value); +} + +QString CSMSettings::Setting::styleSheet() const +{ + return property (Property_StyleSheet).at(0); +} + void CSMSettings::Setting::setPrefix (const QString &value) { setProperty (Property_Prefix, value); @@ -280,14 +291,16 @@ CSMSettings::SettingType CSMSettings::Setting::type() const Property_SettingType).at(0).toInt()); } -void CSMSettings::Setting::setMaximum (int value) +void CSMSettings::Setting::setRange (int min, int max) { - setProperty (Property_Maximum, value); + setProperty (Property_Minimum, min); + setProperty (Property_Maximum, max); } -void CSMSettings::Setting::setMaximum (double value) +void CSMSettings::Setting::setRange (double min, double max) { - setProperty (Property_Maximum, value); + setProperty (Property_Minimum, min); + setProperty (Property_Maximum, max); } QString CSMSettings::Setting::maximum() const @@ -295,16 +308,6 @@ QString CSMSettings::Setting::maximum() const return property (Property_Maximum).at(0); } -void CSMSettings::Setting::setMinimum (int value) -{ - setProperty (Property_Minimum, value); -} - -void CSMSettings::Setting::setMinimum (double value) -{ - setProperty (Property_Minimum, value); -} - QString CSMSettings::Setting::minimum() const { return property (Property_Minimum).at(0); @@ -362,6 +365,26 @@ bool CSMSettings::Setting::wrapping() const return (property (Property_Wrapping).at(0) == "true"); } +void CSMSettings::Setting::setLabel (const QString& label) +{ + setProperty (Property_Label, label); +} + +QString CSMSettings::Setting::getLabel() const +{ + return property (Property_Label).at (0); +} + +void CSMSettings::Setting::setToolTip (const QString& toolTip) +{ + setProperty (Property_ToolTip, toolTip); +} + +QString CSMSettings::Setting::getToolTip() const +{ + return property (Property_ToolTip).at (0); +} + void CSMSettings::Setting::setProperty (SettingProperty prop, bool value) { setProperty (prop, QStringList() << QVariant (value).toString()); diff --git a/apps/opencs/model/settings/setting.hpp b/apps/opencs/model/settings/setting.hpp index e40302f00..be51a531a 100644 --- a/apps/opencs/model/settings/setting.hpp +++ b/apps/opencs/model/settings/setting.hpp @@ -29,8 +29,8 @@ namespace CSMSettings public: - explicit Setting(SettingType typ, const QString &settingName, - const QString &pageName); + Setting(SettingType typ, const QString &settingName, + const QString &pageName, const QString& label = ""); void addProxy (const Setting *setting, const QStringList &vals); void addProxy (const Setting *setting, const QList &list); @@ -66,12 +66,11 @@ namespace CSMSettings void setMask (const QString &value); QString mask() const; - void setMaximum (int value); - void setMaximum (double value); + void setRange (int min, int max); + void setRange (double min, double max); + QString maximum() const; - void setMinimum (int value); - void setMinimum (double value); QString minimum() const; void setName (const QString &value); @@ -80,6 +79,9 @@ namespace CSMSettings void setPage (const QString &value); QString page() const; + void setStyleSheet (const QString &value); + QString styleSheet() const; + void setPrefix (const QString &value); QString prefix() const; @@ -129,6 +131,13 @@ namespace CSMSettings void setWidgetWidth (int value); int widgetWidth() const; + /// This is the text the user gets to see. + void setLabel (const QString& label); + QString getLabel() const; + + void setToolTip (const QString& toolTip); + QString getToolTip() const; + ///returns the specified property value QStringList property (SettingProperty prop) const; diff --git a/apps/opencs/model/settings/support.hpp b/apps/opencs/model/settings/support.hpp index 229e293b8..ab0e5088c 100644 --- a/apps/opencs/model/settings/support.hpp +++ b/apps/opencs/model/settings/support.hpp @@ -35,12 +35,15 @@ namespace CSMSettings Property_TickInterval = 19, Property_TicksAbove = 20, Property_TicksBelow = 21, + Property_StyleSheet = 22, + Property_Label = 23, + Property_ToolTip = 24, //Stringlists should always be the last items - Property_DefaultValues = 22, - Property_DeclaredValues = 23, - Property_DefinedValues = 24, - Property_Proxies = 25 + Property_DefaultValues = 25, + Property_DeclaredValues = 26, + Property_DefinedValues = 27, + Property_Proxies = 28 }; ///Basic setting widget types. @@ -106,7 +109,7 @@ namespace CSMSettings "is_multi_line", "widget_width", "view_row", "view_column", "delimiter", "is_serializable","column_span", "row_span", "minimum", "maximum", "special_value_text", "prefix", "suffix", "single_step", "wrapping", - "tick_interval", "ticks_above", "ticks_below", + "tick_interval", "ticks_above", "ticks_below", "stylesheet", "defaults", "declarations", "definitions", "proxies" }; @@ -135,6 +138,7 @@ namespace CSMSettings "1", //tick interval "false", //ticks above "true", //ticks below + "", //StyleSheet "", //default values "", //declared values "", //defined values diff --git a/apps/opencs/model/settings/usersettings.cpp b/apps/opencs/model/settings/usersettings.cpp index 04f98f0d6..7dac660c3 100644 --- a/apps/opencs/model/settings/usersettings.cpp +++ b/apps/opencs/model/settings/usersettings.cpp @@ -4,12 +4,16 @@ #include #include +#include #include #include "setting.hpp" #include "support.hpp" +#include #include +#include + /** * Workaround for problems with whitespaces in paths in older versions of Boost library */ @@ -26,81 +30,184 @@ namespace boost } /* namespace boost */ #endif /* (BOOST_VERSION <= 104600) */ -CSMSettings::UserSettings *CSMSettings::UserSettings::mUserSettingsInstance = 0; +CSMSettings::UserSettings *CSMSettings::UserSettings::sUserSettingsInstance = 0; -CSMSettings::UserSettings::UserSettings (const Files::ConfigurationManager& configurationManager) -: mCfgMgr (configurationManager) + CSMSettings::UserSettings::UserSettings (const Files::ConfigurationManager& configurationManager) + : mCfgMgr (configurationManager) + , mSettingDefinitions(NULL) { - assert(!mUserSettingsInstance); - mUserSettingsInstance = this; - - mSettingDefinitions = 0; + assert(!sUserSettingsInstance); + sUserSettingsInstance = this; buildSettingModelDefaults(); } void CSMSettings::UserSettings::buildSettingModelDefaults() { - QString section = "Window Size"; - { - Setting *width = createSetting (Type_LineEdit, section, "Width"); - Setting *height = createSetting (Type_LineEdit, section, "Height"); - - width->setWidgetWidth (5); - height->setWidgetWidth (8); - - width->setDefaultValues (QStringList() << "1024"); - height->setDefaultValues (QStringList() << "768"); - - width->setEditorSetting (true); - height->setEditorSetting (true); + QString section; - height->setViewLocation (2,2); - width->setViewLocation (2,1); + declareSection ("3d-render", "3D Rendering"); + { + Setting *shaders = createSetting (Type_CheckBox, "shaders", "Enable Shaders"); + shaders->setDefaultValue ("true"); + + Setting *farClipDist = createSetting (Type_DoubleSpinBox, "far-clip-distance", "Far clipping distance"); + farClipDist->setDefaultValue (300000); + farClipDist->setRange (0, 1000000); + farClipDist->setToolTip ("The maximum distance objects are still rendered at."); + + QString defaultValue = "None"; + Setting *antialiasing = createSetting (Type_ComboBox, "antialiasing", "Antialiasing"); + antialiasing->setDeclaredValues (QStringList() + << defaultValue << "MSAA 2" << "MSAA 4" << "MSAA 8" << "MSAA 16"); + antialiasing->setDefaultValue (defaultValue); + } - /* - *Create the proxy setting for predefined values - */ - Setting *preDefined = createSetting (Type_ComboBox, section, - "Pre-Defined"); + declareSection ("3d-render-adv", "3D Rendering (Advanced)"); + { + Setting *numLights = createSetting (Type_SpinBox, "num_lights", + "Number of lights per pass"); + numLights->setDefaultValue (8); + numLights->setRange (1, 100); + } - preDefined->setDeclaredValues (QStringList() << "640 x 480" - << "800 x 600" << "1024 x 768" << "1440 x 900"); + declareSection ("scene-input", "Scene Input"); + { + Setting *timer = createSetting (Type_SpinBox, "timer", "Input responsiveness"); + timer->setDefaultValue (20); + timer->setRange (1, 100); + timer->setToolTip ("The time between two checks for user input in milliseconds.

" + "Lower value result in higher responsiveness."); + + Setting *fastFactor = createSetting (Type_SpinBox, "fast-factor", + "Fast movement factor"); + fastFactor->setDefaultValue (4); + fastFactor->setRange (1, 100); + fastFactor->setToolTip ( + "Factor by which movement is speed up while the shift key is held down."); + } + declareSection ("window", "Window"); + { + Setting *preDefined = createSetting (Type_ComboBox, "pre-defined", + "Default window size"); + preDefined->setEditorSetting (false); + preDefined->setDeclaredValues ( + QStringList() << "640 x 480" << "800 x 600" << "1024 x 768" << "1440 x 900"); preDefined->setViewLocation (1, 1); - preDefined->setWidgetWidth (10); preDefined->setColumnSpan (2); + preDefined->setToolTip ("Newly opened top-level windows will open with this size " + "(picked from a list of pre-defined values)"); - preDefined->addProxy (width, - QStringList() << "640" << "800" << "1024" << "1440" - ); + Setting *width = createSetting (Type_LineEdit, "default-width", + "Default window width"); + width->setDefaultValues (QStringList() << "1024"); + width->setViewLocation (2, 1); + width->setColumnSpan (1); + width->setToolTip ("Newly opened top-level windows will open with this width."); + preDefined->addProxy (width, QStringList() << "640" << "800" << "1024" << "1440"); - preDefined->addProxy (height, - QStringList() << "480" << "600" << "768" << "900" - ); + Setting *height = createSetting (Type_LineEdit, "default-height", + "Default window height"); + height->setDefaultValues (QStringList() << "768"); + height->setViewLocation (2, 2); + height->setColumnSpan (1); + height->setToolTip ("Newly opened top-level windows will open with this height."); + preDefined->addProxy (height, QStringList() << "480" << "600" << "768" << "900"); + + Setting *reuse = createSetting (Type_CheckBox, "reuse", "Reuse Subviews"); + reuse->setDefaultValue ("true"); + reuse->setToolTip ("When a new subview is requested and a matching subview already " + " exist, do not open a new subview and use the existing one instead."); + + Setting *statusBar = createSetting (Type_CheckBox, "show-statusbar", "Show Status Bar"); + statusBar->setDefaultValue ("true"); + statusBar->setToolTip ("If a newly open top level window is showing status bars or not. " + " Note that this does not affect existing windows."); + + Setting *maxSubView = createSetting (Type_SpinBox, "max-subviews", + "Maximum number of subviews per top-level window"); + maxSubView->setDefaultValue (256); + maxSubView->setRange (1, 256); + maxSubView->setToolTip ("If the maximum number is reached and a new subview is opened " + "it will be placed into a new top-level window."); + + Setting *hide = createSetting (Type_CheckBox, "hide-subview", "Hide single subview"); + hide->setDefaultValue ("false"); + hide->setToolTip ("When a view contains only a single subview, hide the subview title " + "bar and if this subview is closed also close the view (unless it is the last " + "view for this document)"); + + Setting *minWidth = createSetting (Type_SpinBox, "minimum-width", + "Minimum subview width"); + minWidth->setDefaultValue (325); + minWidth->setRange (50, 10000); + minWidth->setToolTip ("Minimum width of subviews."); } - section = "Display Format"; + declareSection ("records", "Records"); { QString defaultValue = "Icon and Text"; + QStringList values = QStringList() << defaultValue << "Icon Only" << "Text Only"; - QStringList values = QStringList() - << defaultValue << "Icon Only" << "Text Only"; - - Setting *rsd = createSetting (Type_RadioButton, - section, "Record Status Display"); - - Setting *ritd = createSetting (Type_RadioButton, - section, "Referenceable ID Type Display"); - + Setting *rsd = createSetting (Type_RadioButton, "status-format", + "Modification status display format"); + rsd->setDefaultValue (defaultValue); rsd->setDeclaredValues (values); + + Setting *ritd = createSetting (Type_RadioButton, "type-format", + "ID type display format"); + ritd->setDefaultValue (defaultValue); ritd->setDeclaredValues (values); + } - rsd->setEditorSetting (true); - ritd->setEditorSetting (true); + declareSection ("table-input", "Table Input"); + { + QString inPlaceEdit ("Edit in Place"); + QString editRecord ("Edit Record"); + QString view ("View"); + QString editRecordAndClose ("Edit Record and Close"); + + QStringList values; + values + << "None" << inPlaceEdit << editRecord << view << "Revert" << "Delete" + << editRecordAndClose << "View and Close"; + + QString toolTip = "

    " + "
  • None
  • " + "
  • Edit in Place: Edit the clicked cell
  • " + "
  • Edit Record: Open a dialogue subview for the clicked record
  • " + "
  • View: Open a scene subview for the clicked record (not available everywhere)
  • " + "
  • Revert: Revert record
  • " + "
  • Delete: Delete recordy
  • " + "
  • Edit Record and Close: Open a dialogue subview for the clicked record and close the table subview
  • " + "
  • View And Close: Open a scene subview for the clicked record and close the table subview
  • " + "
"; + + Setting *doubleClick = createSetting (Type_ComboBox, "double", "Double Click"); + doubleClick->setDeclaredValues (values); + doubleClick->setDefaultValue (inPlaceEdit); + doubleClick->setToolTip ("Action on double click in table:

" + toolTip); + + Setting *shiftDoubleClick = createSetting (Type_ComboBox, "double-s", + "Shift Double Click"); + shiftDoubleClick->setDeclaredValues (values); + shiftDoubleClick->setDefaultValue (editRecord); + shiftDoubleClick->setToolTip ("Action on shift double click in table:

" + toolTip); + + Setting *ctrlDoubleClick = createSetting (Type_ComboBox, "double-c", + "Control Double Click"); + ctrlDoubleClick->setDeclaredValues (values); + ctrlDoubleClick->setDefaultValue (view); + ctrlDoubleClick->setToolTip ("Action on control double click in table:

" + toolTip); + + Setting *shiftCtrlDoubleClick = createSetting (Type_ComboBox, "double-sc", + "Shift Control Double Click"); + shiftCtrlDoubleClick->setDeclaredValues (values); + shiftCtrlDoubleClick->setDefaultValue (editRecordAndClose); + shiftCtrlDoubleClick->setToolTip ("Action on shift control double click in table:

" + toolTip); } - section = "Proxy Selection Test"; { /****************************************************************** * There are three types of values: @@ -276,7 +383,7 @@ void CSMSettings::UserSettings::buildSettingModelDefaults() CSMSettings::UserSettings::~UserSettings() { - mUserSettingsInstance = 0; + sUserSettingsInstance = 0; } void CSMSettings::UserSettings::loadSettings (const QString &fileName) @@ -307,8 +414,21 @@ void CSMSettings::UserSettings::loadSettings (const QString &fileName) (QSettings::IniFormat, QSettings::UserScope, "opencs", QString(), this); } -bool CSMSettings::UserSettings::hasSettingDefinitions - (const QString &viewKey) const +// if the key is not found create one with a default value +QString CSMSettings::UserSettings::setting(const QString &viewKey, const QString &value) +{ + if(mSettingDefinitions->contains(viewKey)) + return settingValue(viewKey); + else if(value != QString()) + { + mSettingDefinitions->setValue (viewKey, QStringList() << value); + return value; + } + + return QString(); +} + +bool CSMSettings::UserSettings::hasSettingDefinitions (const QString &viewKey) const { return (mSettingDefinitions->contains (viewKey)); } @@ -326,10 +446,12 @@ void CSMSettings::UserSettings::saveDefinitions() const QString CSMSettings::UserSettings::settingValue (const QString &settingKey) { + QStringList defs; + if (!mSettingDefinitions->contains (settingKey)) return QString(); - QStringList defs = mSettingDefinitions->value (settingKey).toStringList(); + defs = mSettingDefinitions->value (settingKey).toStringList(); if (defs.isEmpty()) return QString(); @@ -339,8 +461,8 @@ QString CSMSettings::UserSettings::settingValue (const QString &settingKey) CSMSettings::UserSettings& CSMSettings::UserSettings::instance() { - assert(mUserSettingsInstance); - return *mUserSettingsInstance; + assert(sUserSettingsInstance); + return *sUserSettingsInstance; } void CSMSettings::UserSettings::updateUserSetting(const QString &settingKey, @@ -348,6 +470,15 @@ void CSMSettings::UserSettings::updateUserSetting(const QString &settingKey, { mSettingDefinitions->setValue (settingKey ,list); + if(settingKey == "3d-render-adv/num_lights" && !list.empty()) + { + sh::Factory::getInstance ().setGlobalSetting ("num_lights", list.at(0).toStdString()); + } + else if(settingKey == "3d-render/shaders" && !list.empty()) + { + sh::Factory::getInstance ().setShadersEnabled (list.at(0).toStdString() == "true" ? true : false); + } + emit userSettingUpdated (settingKey, list); } @@ -387,30 +518,62 @@ void CSMSettings::UserSettings::removeSetting } } - CSMSettings::SettingPageMap CSMSettings::UserSettings::settingPageMap() const { SettingPageMap pageMap; foreach (Setting *setting, mSettings) - pageMap[setting->page()].append (setting); + { + SettingPageMap::iterator iter = pageMap.find (setting->page()); + + if (iter==pageMap.end()) + { + QPair > value; + + std::map::const_iterator iter2 = + mSectionLabels.find (setting->page()); + + value.first = iter2!=mSectionLabels.end() ? iter2->second : ""; + + iter = pageMap.insert (setting->page(), value); + } + + iter->second.append (setting); + } return pageMap; } CSMSettings::Setting *CSMSettings::UserSettings::createSetting - (CSMSettings::SettingType typ, const QString &page, const QString &name) + (CSMSettings::SettingType type, const QString &name, const QString& label) { - //get list of all settings for the current setting name - if (findSetting (page, name)) - { - qWarning() << "Duplicate declaration encountered: " - << (name + '/' + page); - return 0; - } + Setting *setting = new Setting (type, name, mSection, label); + + // set useful defaults + int row = 1; + + if (!mSettings.empty()) + row = mSettings.back()->viewRow()+1; + + setting->setViewLocation (row, 1); + + setting->setColumnSpan (3); - Setting *setting = new Setting (typ, name, page); + int width = 10; + if (type==Type_CheckBox) + width = 40; + + setting->setWidgetWidth (width); + + if (type==Type_CheckBox) + setting->setStyleSheet ("QGroupBox { border: 0px; }"); + + if (type==Type_CheckBox) + setting->setDeclaredValues(QStringList() << "true" << "false"); + + if (type==Type_CheckBox) + setting->setSpecialValueText (setting->getLabel()); //add declaration to the model mSettings.append (setting); @@ -418,6 +581,12 @@ CSMSettings::Setting *CSMSettings::UserSettings::createSetting return setting; } +void CSMSettings::UserSettings::declareSection (const QString& page, const QString& label) +{ + mSection = page; + mSectionLabels[page] = label; +} + QStringList CSMSettings::UserSettings::definitions (const QString &viewKey) const { if (mSettingDefinitions->contains (viewKey)) diff --git a/apps/opencs/model/settings/usersettings.hpp b/apps/opencs/model/settings/usersettings.hpp index 7e553caf6..5188a9842 100644 --- a/apps/opencs/model/settings/usersettings.hpp +++ b/apps/opencs/model/settings/usersettings.hpp @@ -1,10 +1,13 @@ #ifndef USERSETTINGS_HPP #define USERSETTINGS_HPP +#include + #include #include #include #include +#include #include #include "support.hpp" @@ -22,18 +25,20 @@ class QSettings; namespace CSMSettings { class Setting; - typedef QMap > SettingPageMap; + typedef QMap > > SettingPageMap; class UserSettings: public QObject { Q_OBJECT - static UserSettings *mUserSettingsInstance; + static UserSettings *sUserSettingsInstance; const Files::ConfigurationManager& mCfgMgr; QSettings *mSettingDefinitions; QList mSettings; + QString mSection; + std::map mSectionLabels; public: @@ -62,7 +67,7 @@ namespace CSMSettings { void removeSetting (const QString &pageName, const QString &settingName); - ///Retreive a map of the settings, keyed by page name + ///Retrieve a map of the settings, keyed by page name SettingPageMap settingPageMap() const; ///Returns a string list of defined vlaues for the specified setting @@ -75,13 +80,20 @@ namespace CSMSettings { ///Save any unsaved changes in the QSettings object void saveDefinitions() const; + QString setting(const QString &viewKey, const QString &value = QString()); + private: void buildSettingModelDefaults(); ///add a new setting to the model and return it - Setting *createSetting (CSMSettings::SettingType typ, - const QString &page, const QString &name); + Setting *createSetting (CSMSettings::SettingType type, const QString &name, + const QString& label); + + /// Set the section for createSetting calls. + /// + /// Sections can be declared multiple times. + void declareSection (const QString& page, const QString& label); signals: diff --git a/apps/opencs/model/tools/bodypartcheck.cpp b/apps/opencs/model/tools/bodypartcheck.cpp new file mode 100644 index 000000000..a26945acf --- /dev/null +++ b/apps/opencs/model/tools/bodypartcheck.cpp @@ -0,0 +1,51 @@ +#include "bodypartcheck.hpp" + +CSMTools::BodyPartCheckStage::BodyPartCheckStage( + const CSMWorld::IdCollection &bodyParts, + const CSMWorld::Resources &meshes, + const CSMWorld::IdCollection &races ) : + mBodyParts(bodyParts), + mMeshes(meshes), + mRaces(races) +{ } + +int CSMTools::BodyPartCheckStage::setup() +{ + return mBodyParts.getSize(); +} + +void CSMTools::BodyPartCheckStage::perform ( int stage, Messages &messages ) +{ + const CSMWorld::Record &record = mBodyParts.getRecord(stage); + + if ( record.isDeleted() ) + return; + + const ESM::BodyPart &bodyPart = record.get(); + + CSMWorld::UniversalId id( CSMWorld::UniversalId::Type_BodyPart, bodyPart.mId ); + + // Check BYDT + if (bodyPart.mData.mPart > 14 ) + messages.push_back(std::make_pair( id, bodyPart.mId + " has out of range part value." )); + + if (bodyPart.mData.mFlags > 3 ) + messages.push_back(std::make_pair( id, bodyPart.mId + " has out of range flags value." )); + + if (bodyPart.mData.mType > 2 ) + messages.push_back(std::make_pair( id, bodyPart.mId + " has out of range type value." )); + + // Check MODL + + if ( bodyPart.mModel.empty() ) + messages.push_back(std::make_pair( id, bodyPart.mId + " has no model." )); + else if ( mMeshes.searchId( bodyPart.mModel ) == -1 ) + messages.push_back(std::make_pair( id, bodyPart.mId + " has invalid model." )); + + // Check FNAM + + if ( bodyPart.mRace.empty() ) + messages.push_back(std::make_pair( id, bodyPart.mId + " has no race." )); + else if ( mRaces.searchId( bodyPart.mRace ) == -1 ) + messages.push_back(std::make_pair( id, bodyPart.mId + " has invalid race." )); +} diff --git a/apps/opencs/model/tools/bodypartcheck.hpp b/apps/opencs/model/tools/bodypartcheck.hpp new file mode 100644 index 000000000..d72badfdf --- /dev/null +++ b/apps/opencs/model/tools/bodypartcheck.hpp @@ -0,0 +1,35 @@ +#ifndef CSM_TOOLS_BODYPARTCHECK_H +#define CSM_TOOLS_BODYPARTCHECK_H + +#include +#include + +#include "../world/resources.hpp" +#include "../world/idcollection.hpp" + +#include "../doc/stage.hpp" + +namespace CSMTools +{ + /// \brief VerifyStage: make sure that body part records are internally consistent + class BodyPartCheckStage : public CSMDoc::Stage + { + const CSMWorld::IdCollection &mBodyParts; + const CSMWorld::Resources &mMeshes; + const CSMWorld::IdCollection &mRaces; + + public: + BodyPartCheckStage( + const CSMWorld::IdCollection &bodyParts, + const CSMWorld::Resources &meshes, + const CSMWorld::IdCollection &races ); + + virtual int setup(); + ///< \return number of steps + + virtual void perform( int stage, Messages &messages ); + ///< Messages resulting from this tage will be appended to \a messages. + }; +} + +#endif \ No newline at end of file diff --git a/apps/opencs/model/tools/factioncheck.cpp b/apps/opencs/model/tools/factioncheck.cpp index 42d577163..ba8cfe1f9 100644 --- a/apps/opencs/model/tools/factioncheck.cpp +++ b/apps/opencs/model/tools/factioncheck.cpp @@ -42,7 +42,7 @@ void CSMTools::FactionCheckStage::perform (int stage, Messages& messages) // test for non-unique skill std::map skills; // ID, number of occurrences - for (int i=0; i<6; ++i) + for (int i=0; i<7; ++i) if (faction.mData.mSkills[i]!=-1) ++skills[faction.mData.mSkills[i]]; @@ -54,4 +54,4 @@ void CSMTools::FactionCheckStage::perform (int stage, Messages& messages) } /// \todo check data members that can't be edited in the table view -} \ No newline at end of file +} diff --git a/apps/opencs/model/tools/referenceablecheck.cpp b/apps/opencs/model/tools/referenceablecheck.cpp index 488081f46..1816d0808 100644 --- a/apps/opencs/model/tools/referenceablecheck.cpp +++ b/apps/opencs/model/tools/referenceablecheck.cpp @@ -621,12 +621,6 @@ void CSMTools::ReferenceableCheckStage::npcCheck ( } else { - if (npc.mNpdt52.mMana < 0) - messages.push_back (std::make_pair (id, npc.mId + " mana has negative value")); - - if (npc.mNpdt52.mFatigue < 0) - messages.push_back (std::make_pair (id, npc.mId + " fatigue has negative value")); - if (npc.mNpdt52.mAgility == 0) messages.push_back (std::make_pair (id, npc.mId + " agility has zero value")); diff --git a/apps/opencs/model/tools/scriptcheck.cpp b/apps/opencs/model/tools/scriptcheck.cpp index b989e22a2..d2c647bda 100644 --- a/apps/opencs/model/tools/scriptcheck.cpp +++ b/apps/opencs/model/tools/scriptcheck.cpp @@ -7,6 +7,8 @@ #include #include +#include "../doc/document.hpp" + #include "../world/data.hpp" void CSMTools::ScriptCheckStage::report (const std::string& message, const Compiler::TokenLoc& loc, @@ -37,8 +39,8 @@ void CSMTools::ScriptCheckStage::report (const std::string& message, Type type) (type==ErrorMessage ? "error: " : "warning: ") + message)); } -CSMTools::ScriptCheckStage::ScriptCheckStage (const CSMWorld::Data& data) -: mData (data), mContext (data), mMessages (0) +CSMTools::ScriptCheckStage::ScriptCheckStage (const CSMDoc::Document& document) +: mDocument (document), mContext (document.getData()), mMessages (0) { /// \todo add an option to configure warning mode setWarningsMode (0); @@ -53,18 +55,25 @@ int CSMTools::ScriptCheckStage::setup() mMessages = 0; mId.clear(); - return mData.getScripts().getSize(); + return mDocument.getData().getScripts().getSize(); } void CSMTools::ScriptCheckStage::perform (int stage, Messages& messages) { + mId = mDocument.getData().getScripts().getId (stage); + + if (mDocument.isBlacklisted ( + CSMWorld::UniversalId (CSMWorld::UniversalId::Type_Script, mId))) + return; + mMessages = &messages; - mId = mData.getScripts().getId (stage); try { - mFile = mData.getScripts().getRecord (stage).get().mId; - std::istringstream input (mData.getScripts().getRecord (stage).get().mScriptText); + const CSMWorld::Data& data = mDocument.getData(); + + mFile = data.getScripts().getRecord (stage).get().mId; + std::istringstream input (data.getScripts().getRecord (stage).get().mScriptText); Compiler::Scanner scanner (*this, input, mContext.getExtensions()); diff --git a/apps/opencs/model/tools/scriptcheck.hpp b/apps/opencs/model/tools/scriptcheck.hpp index ecf8d61b7..75f11b9d4 100644 --- a/apps/opencs/model/tools/scriptcheck.hpp +++ b/apps/opencs/model/tools/scriptcheck.hpp @@ -8,12 +8,17 @@ #include "../world/scriptcontext.hpp" +namespace CSMDoc +{ + class Document; +} + namespace CSMTools { /// \brief VerifyStage: make sure that scripts compile class ScriptCheckStage : public CSMDoc::Stage, private Compiler::ErrorHandler { - const CSMWorld::Data& mData; + const CSMDoc::Document& mDocument; Compiler::Extensions mExtensions; CSMWorld::ScriptContext mContext; std::string mId; @@ -28,7 +33,7 @@ namespace CSMTools public: - ScriptCheckStage (const CSMWorld::Data& data); + ScriptCheckStage (const CSMDoc::Document& document); virtual int setup(); ///< \return number of steps diff --git a/apps/opencs/model/tools/tools.cpp b/apps/opencs/model/tools/tools.cpp index 2f93db48e..79b0b18b0 100644 --- a/apps/opencs/model/tools/tools.cpp +++ b/apps/opencs/model/tools/tools.cpp @@ -5,6 +5,7 @@ #include "../doc/state.hpp" #include "../doc/operation.hpp" +#include "../doc/document.hpp" #include "../world/data.hpp" #include "../world/universalid.hpp" @@ -21,6 +22,7 @@ #include "spellcheck.hpp" #include "referenceablecheck.hpp" #include "scriptcheck.hpp" +#include "bodypartcheck.hpp" CSMDoc::Operation *CSMTools::Tools::get (int type) { @@ -44,7 +46,7 @@ CSMDoc::Operation *CSMTools::Tools::getVerifier() mVerifier = new CSMDoc::Operation (CSMDoc::State_Verifying, false); connect (mVerifier, SIGNAL (progress (int, int, int)), this, SIGNAL (progress (int, int, int))); - connect (mVerifier, SIGNAL (done (int)), this, SIGNAL (done (int))); + connect (mVerifier, SIGNAL (done (int, bool)), this, SIGNAL (done (int, bool))); connect (mVerifier, SIGNAL (reportMessage (const CSMWorld::UniversalId&, const std::string&, int)), this, SLOT (verifierMessage (const CSMWorld::UniversalId&, const std::string&, int))); @@ -78,15 +80,23 @@ CSMDoc::Operation *CSMTools::Tools::getVerifier() mVerifier->appendStage (new SpellCheckStage (mData.getSpells())); - mVerifier->appendStage (new ReferenceableCheckStage (mData.getReferenceables().getDataSet(), mData.getRaces(), mData.getClasses(), mData.getFactions())); + mVerifier->appendStage (new ReferenceableCheckStage (mData.getReferenceables().getDataSet(), mData.getRaces(), mData.getClasses(), mData.getFactions())); - mVerifier->appendStage (new ScriptCheckStage (mData)); + mVerifier->appendStage (new ScriptCheckStage (mDocument)); + + mVerifier->appendStage( + new BodyPartCheckStage( + mData.getBodyParts(), + mData.getResources( + CSMWorld::UniversalId( CSMWorld::UniversalId::Type_Meshes )), + mData.getRaces() )); } return mVerifier; } -CSMTools::Tools::Tools (CSMWorld::Data& data) : mData (data), mVerifier (0), mNextReportNumber (0) +CSMTools::Tools::Tools (CSMDoc::Document& document) +: mDocument (document), mData (document.getData()), mVerifier (0), mNextReportNumber (0) { // index 0: load error log mReports.insert (std::make_pair (mNextReportNumber++, new ReportModel)); diff --git a/apps/opencs/model/tools/tools.hpp b/apps/opencs/model/tools/tools.hpp index 3394d3f62..7ca30e6b9 100644 --- a/apps/opencs/model/tools/tools.hpp +++ b/apps/opencs/model/tools/tools.hpp @@ -14,6 +14,7 @@ namespace CSMWorld namespace CSMDoc { class Operation; + class Document; } namespace CSMTools @@ -24,6 +25,7 @@ namespace CSMTools { Q_OBJECT + CSMDoc::Document& mDocument; CSMWorld::Data& mData; CSMDoc::Operation *mVerifier; std::map mReports; @@ -44,7 +46,7 @@ namespace CSMTools public: - Tools (CSMWorld::Data& data); + Tools (CSMDoc::Document& document); virtual ~Tools(); @@ -68,7 +70,7 @@ namespace CSMTools void progress (int current, int max, int type); - void done (int type); + void done (int type, bool failed); }; } diff --git a/apps/opencs/model/world/cell.cpp b/apps/opencs/model/world/cell.cpp index cd58fca1e..40520a9ba 100644 --- a/apps/opencs/model/world/cell.cpp +++ b/apps/opencs/model/world/cell.cpp @@ -18,8 +18,3 @@ void CSMWorld::Cell::load (ESM::ESMReader &esm) mId = stream.str(); } } - -void CSMWorld::Cell::addRef (const std::string& id) -{ - mRefs.push_back (std::make_pair (id, false)); -} \ No newline at end of file diff --git a/apps/opencs/model/world/cell.hpp b/apps/opencs/model/world/cell.hpp index e6f3c8c35..a47dbf45d 100644 --- a/apps/opencs/model/world/cell.hpp +++ b/apps/opencs/model/world/cell.hpp @@ -15,12 +15,9 @@ namespace CSMWorld struct Cell : public ESM::Cell { std::string mId; - std::vector > mRefs; // ID, modified - std::vector mDeletedRefs; void load (ESM::ESMReader &esm); - void addRef (const std::string& id); }; } diff --git a/apps/opencs/model/world/columnbase.hpp b/apps/opencs/model/world/columnbase.hpp index fe310d0aa..db9b8b3c6 100644 --- a/apps/opencs/model/world/columnbase.hpp +++ b/apps/opencs/model/world/columnbase.hpp @@ -68,6 +68,7 @@ namespace CSMWorld Display_TopicInfo, Display_JournalInfo, Display_Scene, + Display_GlobalVariable, //CONCRETE TYPES ENDS HERE Display_Integer, @@ -89,7 +90,20 @@ namespace CSMWorld Display_RefRecordType, Display_DialogueType, Display_QuestStatusType, - Display_Gender + Display_EnchantmentType, + Display_BodyPartType, + Display_MeshType, + Display_Gender, + Display_Mesh, + Display_Icon, + Display_Music, + Display_SoundRes, + Display_Texture, + Display_Video, + Display_Colour, + Display_ScriptLines, // console context + Display_SoundGeneratorType, + Display_School }; int mColumnId; @@ -113,8 +127,6 @@ namespace CSMWorld template struct Column : public ColumnBase { - int mFlags; - Column (int columnId, Display displayType, int flags = Flag_Table | Flag_Dialogue) : ColumnBase (columnId, displayType, flags) {} diff --git a/apps/opencs/model/world/columnimp.hpp b/apps/opencs/model/world/columnimp.hpp index 6976b454d..6f830e6e3 100644 --- a/apps/opencs/model/world/columnimp.hpp +++ b/apps/opencs/model/world/columnimp.hpp @@ -1,7 +1,9 @@ #ifndef CSM_WOLRD_COLUMNIMP_H #define CSM_WOLRD_COLUMNIMP_H +#include #include +#include #include @@ -463,14 +465,21 @@ namespace CSMWorld struct FlagColumn : public Column { int mMask; + bool mInverted; - FlagColumn (int columnId, int mask) - : Column (columnId, ColumnBase::Display_Boolean), mMask (mask) + FlagColumn (int columnId, int mask, bool inverted = false) + : Column (columnId, ColumnBase::Display_Boolean), mMask (mask), + mInverted (inverted) {} virtual QVariant get (const Record& record) const { - return (record.get().mData.mFlags & mMask)!=0; + bool flag = (record.get().mData.mFlags & mMask)!=0; + + if (mInverted) + flag = !flag; + + return flag; } virtual void set (Record& record, const QVariant& data) @@ -479,7 +488,7 @@ namespace CSMWorld int flags = record2.mData.mFlags & ~mMask; - if (data.toInt()) + if ((data.toInt()!=0)!=mInverted) flags |= mMask; record2.mData.mFlags = flags; @@ -493,6 +502,47 @@ namespace CSMWorld } }; + template + struct FlagColumn2 : public Column + { + int mMask; + bool mInverted; + + FlagColumn2 (int columnId, int mask, bool inverted = false) + : Column (columnId, ColumnBase::Display_Boolean), mMask (mask), + mInverted (inverted) + {} + + virtual QVariant get (const Record& record) const + { + bool flag = (record.get().mFlags & mMask)!=0; + + if (mInverted) + flag = !flag; + + return flag; + } + + virtual void set (Record& record, const QVariant& data) + { + ESXRecordT record2 = record.get(); + + int flags = record2.mFlags & ~mMask; + + if ((data.toInt()!=0)!=mInverted) + flags |= mMask; + + record2.mFlags = flags; + + record.setModified (record2); + } + + virtual bool isEditable() const + { + return true; + } + }; + template struct WeightHeightColumn : public Column { @@ -597,7 +647,7 @@ namespace CSMWorld struct SoundFileColumn : public Column { SoundFileColumn() - : Column (Columns::ColumnId_SoundFile, ColumnBase::Display_Sound) + : Column (Columns::ColumnId_SoundFile, ColumnBase::Display_SoundRes) {} virtual QVariant get (const Record& record) const @@ -627,7 +677,7 @@ namespace CSMWorld { /// \todo Replace Display_Integer with something that displays the colour value more directly. MapColourColumn() - : Column (Columns::ColumnId_MapColour, ColumnBase::Display_Integer) + : Column (Columns::ColumnId_MapColour, ColumnBase::Display_Colour) {} virtual QVariant get (const Record& record) const @@ -759,8 +809,18 @@ namespace CSMWorld template struct ScriptColumn : public Column { - ScriptColumn() - : Column (Columns::ColumnId_ScriptText, ColumnBase::Display_Script, 0) {} + enum Type + { + Type_File, // regular script record + Type_Lines, // console context + Type_Info // dialogue context (not implemented yet) + }; + + ScriptColumn (Type type) + : Column (Columns::ColumnId_ScriptText, + type==Type_File ? ColumnBase::Display_Script : ColumnBase::Display_ScriptLines, + type==Type_File ? 0 : ColumnBase::Flag_Dialogue) + {} virtual QVariant get (const Record& record) const { @@ -970,13 +1030,13 @@ namespace CSMWorld virtual QVariant get (const Record& record) const { - return record.get().mFactIndex; + return record.get().mFactionRank; } virtual void set (Record& record, const QVariant& data) { ESXRecordT record2 = record.get(); - record2.mFactIndex = data.toInt(); + record2.mFactionRank = data.toInt(); record.setModified (record2); } @@ -1217,36 +1277,6 @@ namespace CSMWorld } }; - template - struct ScopeColumn : public Column - { - ScopeColumn() - : Column (Columns::ColumnId_Scope, ColumnBase::Display_Integer, 0) - {} - - virtual QVariant get (const Record& record) const - { - return static_cast (record.get().mScope); - } - - virtual void set (Record& record, const QVariant& data) - { - ESXRecordT record2 = record.get(); - record2.mScope = static_cast (data.toInt()); - record.setModified (record2); - } - - virtual bool isEditable() const - { - return true; - } - - virtual bool isUserEditable() const - { - return false; - } - }; - template struct PosColumn : public Column @@ -1269,7 +1299,7 @@ namespace CSMWorld { ESXRecordT record2 = record.get(); - ESM::Position& position = record.get().*mPosition; + ESM::Position& position = record2.*mPosition; position.pos[mIndex] = data.toFloat(); @@ -1303,7 +1333,7 @@ namespace CSMWorld { ESXRecordT record2 = record.get(); - ESM::Position& position = record.get().*mPosition; + ESM::Position& position = record2.*mPosition; position.rot[mIndex] = data.toFloat(); @@ -1682,6 +1712,559 @@ namespace CSMWorld return true; } }; + + template + struct EnchantmentTypeColumn : public Column + { + EnchantmentTypeColumn() + : Column (Columns::ColumnId_EnchantmentType, ColumnBase::Display_EnchantmentType) + {} + + virtual QVariant get (const Record& record) const + { + return static_cast (record.get().mData.mType); + } + + virtual void set (Record& record, const QVariant& data) + { + ESXRecordT record2 = record.get(); + + record2.mData.mType = data.toInt(); + + record.setModified (record2); + } + + virtual bool isEditable() const + { + return true; + } + }; + + template + struct ChargesColumn2 : public Column + { + ChargesColumn2() : Column (Columns::ColumnId_Charges, ColumnBase::Display_Integer) {} + + virtual QVariant get (const Record& record) const + { + return record.get().mData.mCharge; + } + + virtual void set (Record& record, const QVariant& data) + { + ESXRecordT record2 = record.get(); + record2.mData.mCharge = data.toInt(); + record.setModified (record2); + } + + virtual bool isEditable() const + { + return true; + } + }; + + template + struct AutoCalcColumn : public Column + { + AutoCalcColumn() : Column (Columns::ColumnId_AutoCalc, ColumnBase::Display_Boolean) + {} + + virtual QVariant get (const Record& record) const + { + return record.get().mData.mAutocalc!=0; + } + + virtual void set (Record& record, const QVariant& data) + { + ESXRecordT record2 = record.get(); + + record2.mData.mAutocalc = data.toInt(); + + record.setModified (record2); + } + + virtual bool isEditable() const + { + return true; + } + }; + + template + struct ModelColumn : public Column + { + ModelColumn() : Column (Columns::ColumnId_Model, ColumnBase::Display_String) {} + + virtual QVariant get (const Record& record) const + { + return QString::fromUtf8 (record.get().mModel.c_str()); + } + + virtual void set (Record& record, const QVariant& data) + { + ESXRecordT record2 = record.get(); + + record2.mModel = data.toString().toUtf8().constData(); + + record.setModified (record2); + } + + virtual bool isEditable() const + { + return true; + } + }; + + template + struct VampireColumn : public Column + { + VampireColumn() : Column (Columns::ColumnId_Vampire, ColumnBase::Display_Boolean) + {} + + virtual QVariant get (const Record& record) const + { + return record.get().mData.mVampire!=0; + } + + virtual void set (Record& record, const QVariant& data) + { + ESXRecordT record2 = record.get(); + + record2.mData.mVampire = data.toInt(); + + record.setModified (record2); + } + + virtual bool isEditable() const + { + return true; + } + }; + + template + struct BodyPartTypeColumn : public Column + { + BodyPartTypeColumn() + : Column (Columns::ColumnId_BodyPartType, ColumnBase::Display_BodyPartType) + {} + + virtual QVariant get (const Record& record) const + { + return static_cast (record.get().mData.mPart); + } + + virtual void set (Record& record, const QVariant& data) + { + ESXRecordT record2 = record.get(); + + record2.mData.mPart = data.toInt(); + + record.setModified (record2); + } + + virtual bool isEditable() const + { + return true; + } + }; + + template + struct MeshTypeColumn : public Column + { + MeshTypeColumn() + : Column (Columns::ColumnId_MeshType, ColumnBase::Display_MeshType) + {} + + virtual QVariant get (const Record& record) const + { + return static_cast (record.get().mData.mType); + } + + virtual void set (Record& record, const QVariant& data) + { + ESXRecordT record2 = record.get(); + + record2.mData.mType = data.toInt(); + + record.setModified (record2); + } + + virtual bool isEditable() const + { + return true; + } + }; + + template + struct OwnerGlobalColumn : public Column + { + OwnerGlobalColumn() + : Column (Columns::ColumnId_OwnerGlobal, ColumnBase::Display_GlobalVariable) + {} + + virtual QVariant get (const Record& record) const + { + return QString::fromUtf8 (record.get().mGlobalVariable.c_str()); + } + + virtual void set (Record& record, const QVariant& data) + { + ESXRecordT record2 = record.get(); + + record2.mGlobalVariable = data.toString().toUtf8().constData(); + + record.setModified (record2); + } + + virtual bool isEditable() const + { + return true; + } + }; + + template + struct RefNumCounterColumn : public Column + { + RefNumCounterColumn() + : Column (Columns::ColumnId_RefNumCounter, ColumnBase::Display_Integer, 0) + {} + + virtual QVariant get (const Record& record) const + { + return static_cast (record.get().mRefNumCounter); + } + + virtual void set (Record& record, const QVariant& data) + { + ESXRecordT record2 = record.get(); + + record2.mRefNumCounter = data.toInt(); + + record.setModified (record2); + } + + virtual bool isEditable() const + { + return true; + } + + virtual bool isUserEditable() const + { + return false; + } + }; + + template + struct RefNumColumn : public Column + { + RefNumColumn() + : Column (Columns::ColumnId_RefNum, ColumnBase::Display_Integer, 0) + {} + + virtual QVariant get (const Record& record) const + { + return static_cast (record.get().mRefNum.mIndex); + } + + virtual void set (Record& record, const QVariant& data) + { + ESXRecordT record2 = record.get(); + + record2.mRefNum.mIndex = data.toInt(); + + record.setModified (record2); + } + + virtual bool isEditable() const + { + return true; + } + + virtual bool isUserEditable() const + { + return false; + } + }; + + template + struct SoundColumn : public Column + { + SoundColumn() + : Column (Columns::ColumnId_Sound, ColumnBase::Display_Sound) + {} + + virtual QVariant get (const Record& record) const + { + return QString::fromUtf8 (record.get().mSound.c_str()); + } + + virtual void set (Record& record, const QVariant& data) + { + ESXRecordT record2 = record.get(); + + record2.mSound = data.toString().toUtf8().constData(); + + record.setModified (record2); + } + + virtual bool isEditable() const + { + return true; + } + }; + + template + struct CreatureColumn : public Column + { + CreatureColumn() + : Column (Columns::ColumnId_Creature, ColumnBase::Display_Creature) + {} + + virtual QVariant get (const Record& record) const + { + return QString::fromUtf8 (record.get().mCreature.c_str()); + } + + virtual void set (Record& record, const QVariant& data) + { + ESXRecordT record2 = record.get(); + + record2.mCreature = data.toString().toUtf8().constData(); + + record.setModified (record2); + } + + virtual bool isEditable() const + { + return true; + } + }; + + template + struct SoundGeneratorTypeColumn : public Column + { + SoundGeneratorTypeColumn() + : Column (Columns::ColumnId_SoundGeneratorType, ColumnBase::Display_SoundGeneratorType) + {} + + virtual QVariant get (const Record& record) const + { + return static_cast (record.get().mType); + } + + virtual void set (Record& record, const QVariant& data) + { + ESXRecordT record2 = record.get(); + + record2.mType = data.toInt(); + + record.setModified (record2); + } + + virtual bool isEditable() const + { + return true; + } + }; + + template + struct BaseCostColumn : public Column + { + BaseCostColumn() : Column (Columns::ColumnId_BaseCost, ColumnBase::Display_Float) {} + + virtual QVariant get (const Record& record) const + { + return record.get().mData.mBaseCost; + } + + virtual void set (Record& record, const QVariant& data) + { + ESXRecordT record2 = record.get(); + record2.mData.mBaseCost = data.toFloat(); + record.setModified (record2); + } + + virtual bool isEditable() const + { + return true; + } + }; + + template + struct SchoolColumn : public Column + { + SchoolColumn() + : Column (Columns::ColumnId_School, ColumnBase::Display_School) + {} + + virtual QVariant get (const Record& record) const + { + return record.get().mData.mSchool; + } + + virtual void set (Record& record, const QVariant& data) + { + ESXRecordT record2 = record.get(); + + record2.mData.mSchool = data.toInt(); + + record.setModified (record2); + } + + virtual bool isEditable() const + { + return true; + } + }; + + template + struct EffectTextureColumn : public Column + { + EffectTextureColumn (Columns::ColumnId columnId) + : Column (columnId, ColumnBase::Display_Texture) + { + assert (this->mColumnId==Columns::ColumnId_Icon || + this->mColumnId==Columns::ColumnId_Particle); + } + + virtual QVariant get (const Record& record) const + { + return QString::fromUtf8 ( + (this->mColumnId==Columns::ColumnId_Icon ? + record.get().mIcon : record.get().mParticle).c_str()); + } + + virtual void set (Record& record, const QVariant& data) + { + ESXRecordT record2 = record.get(); + + (this->mColumnId==Columns::ColumnId_Icon ? + record2.mIcon : record2.mParticle) + = data.toString().toUtf8().constData(); + + record.setModified (record2); + } + + virtual bool isEditable() const + { + return true; + } + }; + + template + struct EffectObjectColumn : public Column + { + EffectObjectColumn (Columns::ColumnId columnId) + : Column (columnId, columnId==Columns::ColumnId_BoltObject ? ColumnBase::Display_Weapon : ColumnBase::Display_Static) + { + assert (this->mColumnId==Columns::ColumnId_CastingObject || + this->mColumnId==Columns::ColumnId_HitObject || + this->mColumnId==Columns::ColumnId_AreaObject || + this->mColumnId==Columns::ColumnId_BoltObject); + } + + virtual QVariant get (const Record& record) const + { + const std::string *string = 0; + + switch (this->mColumnId) + { + case Columns::ColumnId_CastingObject: string = &record.get().mCasting; break; + case Columns::ColumnId_HitObject: string = &record.get().mHit; break; + case Columns::ColumnId_AreaObject: string = &record.get().mArea; break; + case Columns::ColumnId_BoltObject: string = &record.get().mBolt; break; + } + + if (!string) + throw std::logic_error ("Unsupported column ID"); + + return QString::fromUtf8 (string->c_str()); + } + + virtual void set (Record& record, const QVariant& data) + { + std::string *string = 0; + + ESXRecordT record2 = record.get(); + + switch (this->mColumnId) + { + case Columns::ColumnId_CastingObject: string = &record2.mCasting; break; + case Columns::ColumnId_HitObject: string = &record2.mHit; break; + case Columns::ColumnId_AreaObject: string = &record2.mArea; break; + case Columns::ColumnId_BoltObject: string = &record2.mBolt; break; + } + + if (!string) + throw std::logic_error ("Unsupported column ID"); + + *string = data.toString().toUtf8().constData(); + + record.setModified (record2); + } + + virtual bool isEditable() const + { + return true; + } + }; + + template + struct EffectSoundColumn : public Column + { + EffectSoundColumn (Columns::ColumnId columnId) + : Column (columnId, ColumnBase::Display_Sound) + { + assert (this->mColumnId==Columns::ColumnId_CastingSound || + this->mColumnId==Columns::ColumnId_HitSound || + this->mColumnId==Columns::ColumnId_AreaSound || + this->mColumnId==Columns::ColumnId_BoltSound); + } + + virtual QVariant get (const Record& record) const + { + const std::string *string = 0; + + switch (this->mColumnId) + { + case Columns::ColumnId_CastingSound: string = &record.get().mCastSound; break; + case Columns::ColumnId_HitSound: string = &record.get().mHitSound; break; + case Columns::ColumnId_AreaSound: string = &record.get().mAreaSound; break; + case Columns::ColumnId_BoltSound: string = &record.get().mBoltSound; break; + } + + if (!string) + throw std::logic_error ("Unsupported column ID"); + + return QString::fromUtf8 (string->c_str()); + } + + virtual void set (Record& record, const QVariant& data) + { + std::string *string = 0; + + ESXRecordT record2 = record.get(); + + switch (this->mColumnId) + { + case Columns::ColumnId_CastingSound: string = &record2.mCastSound; break; + case Columns::ColumnId_HitSound: string = &record2.mHitSound; break; + case Columns::ColumnId_AreaSound: string = &record2.mAreaSound; break; + case Columns::ColumnId_BoltSound: string = &record2.mBoltSound; break; + } + + if (!string) + throw std::logic_error ("Unsupported column ID"); + + *string = data.toString().toUtf8().constData(); + + record.setModified (record2); + } + + virtual bool isEditable() const + { + return true; + } + }; } #endif diff --git a/apps/opencs/model/world/columns.cpp b/apps/opencs/model/world/columns.cpp index 7410780e0..8349eb515 100644 --- a/apps/opencs/model/world/columns.cpp +++ b/apps/opencs/model/world/columns.cpp @@ -172,8 +172,35 @@ namespace CSMWorld { ColumnId_Rank, "Rank" }, { ColumnId_Gender, "Gender" }, { ColumnId_PcRank, "PC Rank" }, - { ColumnId_Scope, "Scope" }, { ColumnId_ReferenceableId, "Referenceable ID" }, + { ColumnId_CombatState, "Combat" }, + { ColumnId_MagicState, "Magic" }, + { ColumnId_StealthState, "Stealth" }, + { ColumnId_EnchantmentType, "Enchantment Type" }, + { ColumnId_Vampire, "Vampire" }, + { ColumnId_BodyPartType, "Bodypart Type" }, + { ColumnId_MeshType, "Mesh Type" }, + { ColumnId_OwnerGlobal, "Owner Global" }, + { ColumnId_DefaultProfile, "Default Profile" }, + { ColumnId_BypassNewGame, "Bypass New Game" }, + { ColumnId_GlobalProfile, "Global Profile" }, + { ColumnId_RefNumCounter, "RefNum Counter" }, + { ColumnId_RefNum, "RefNum" }, + { ColumnId_Creature, "Creature" }, + { ColumnId_SoundGeneratorType, "Sound Generator Type" }, + { ColumnId_AllowSpellmaking, "Allow Spellmaking" }, + { ColumnId_AllowEnchanting, "Allow Enchanting" }, + { ColumnId_BaseCost, "Base Cost" }, + { ColumnId_School, "School" }, + { ColumnId_Particle, "Particle" }, + { ColumnId_CastingObject, "Casting Object" }, + { ColumnId_HitObject, "Hit Object" }, + { ColumnId_AreaObject, "Area Object" }, + { ColumnId_BoltObject, "Bolt Object" }, + { ColumnId_CastingSound, "Casting Sound" }, + { ColumnId_HitSound, "Hit Sound" }, + { ColumnId_AreaSound, "Area Sound" }, + { ColumnId_BoltSound, "Bolt Sound" }, { ColumnId_UseValue1, "Use value 1" }, { ColumnId_UseValue2, "Use value 2" }, @@ -299,6 +326,33 @@ namespace "Male", "Female", 0 }; + static const char *sEnchantmentTypes[] = + { + "Cast Once", "When Strikes", "When Used", "Constant Effect", 0 + }; + + static const char *sBodyPartTypes[] = + { + "Head", "Hair", "Neck", "Chest", "Groin", "Hand", "Wrist", "Forearm", "Upper Arm", + "Foot", "Ankle", "Knee", "Upper Leg", "Clavicle", "Tail", 0 + }; + + static const char *sMeshTypes[] = + { + "Skin", "Clothing", "Armour", 0 + }; + + static const char *sSoundGeneratorType[] = + { + "Left Foot", "Right Foot", "Swim Left", "Swim Right", "Moan", "Roar", "Scream", + "Land", 0 + }; + + static const char *sSchools[] = + { + "Alteration", "Conjuration", "Destruction", "Illusion", "Mysticism", "Restoration", 0 + }; + const char **getEnumNames (CSMWorld::Columns::ColumnId column) { switch (column) @@ -316,6 +370,11 @@ namespace case CSMWorld::Columns::ColumnId_DialogueType: return sDialogueTypeEnums; case CSMWorld::Columns::ColumnId_QuestStatusType: return sQuestStatusTypes; case CSMWorld::Columns::ColumnId_Gender: return sGenderEnums; + case CSMWorld::Columns::ColumnId_EnchantmentType: return sEnchantmentTypes; + case CSMWorld::Columns::ColumnId_BodyPartType: return sBodyPartTypes; + case CSMWorld::Columns::ColumnId_MeshType: return sMeshTypes; + case CSMWorld::Columns::ColumnId_SoundGeneratorType: return sSoundGeneratorType; + case CSMWorld::Columns::ColumnId_School: return sSchools; default: return 0; } diff --git a/apps/opencs/model/world/columns.hpp b/apps/opencs/model/world/columns.hpp index 855e89cad..ca0326655 100644 --- a/apps/opencs/model/world/columns.hpp +++ b/apps/opencs/model/world/columns.hpp @@ -165,9 +165,35 @@ namespace CSMWorld ColumnId_Rank = 152, ColumnId_Gender = 153, ColumnId_PcRank = 154, - ColumnId_Scope = 155, ColumnId_ReferenceableId = 156, - + ColumnId_CombatState = 157, + ColumnId_MagicState = 158, + ColumnId_StealthState = 159, + ColumnId_EnchantmentType = 160, + ColumnId_Vampire = 161, + ColumnId_BodyPartType = 162, + ColumnId_MeshType = 163, + ColumnId_OwnerGlobal = 164, + ColumnId_DefaultProfile = 165, + ColumnId_BypassNewGame = 166, + ColumnId_GlobalProfile = 167, + ColumnId_RefNumCounter = 168, + ColumnId_RefNum = 169, + ColumnId_Creature = 170, + ColumnId_SoundGeneratorType = 171, + ColumnId_AllowSpellmaking = 172, + ColumnId_AllowEnchanting = 173, + ColumnId_BaseCost = 174, + ColumnId_School = 175, + ColumnId_Particle = 176, + ColumnId_CastingObject = 177, + ColumnId_HitObject = 178, + ColumnId_AreaObject = 179, + ColumnId_BoltObject = 180, + ColumnId_CastingSound = 177, + ColumnId_HitSound = 178, + ColumnId_AreaSound = 179, + ColumnId_BoltSound = 180, // Allocated to a separate value range, so we don't get a collision should we ever need // to extend the number of use values. ColumnId_UseValue1 = 0x10000, @@ -212,7 +238,7 @@ namespace CSMWorld bool hasEnums (ColumnId column); std::vector getEnums (ColumnId column); - ///< Returns an empty vector, if \Ʀ column isn't an enum type column. + ///< Returns an empty vector, if \a column isn't an enum type column. } } diff --git a/apps/opencs/model/world/commanddispatcher.cpp b/apps/opencs/model/world/commanddispatcher.cpp new file mode 100644 index 000000000..4e146d87c --- /dev/null +++ b/apps/opencs/model/world/commanddispatcher.cpp @@ -0,0 +1,267 @@ + +#include "commanddispatcher.hpp" + +#include + +#include + +#include "../doc/document.hpp" + +#include "idtable.hpp" +#include "record.hpp" +#include "commands.hpp" + +std::vector CSMWorld::CommandDispatcher::getDeletableRecords() const +{ + std::vector result; + + IdTable& model = dynamic_cast (*mDocument.getData().getTableModel (mId)); + + int stateColumnIndex = model.findColumnIndex (Columns::ColumnId_Modification); + + for (std::vector::const_iterator iter (mSelection.begin()); + iter!=mSelection.end(); ++iter) + { + int row = model.getModelIndex (*iter, 0).row(); + + // check record state + RecordBase::State state = static_cast ( + model.data (model.index (row, stateColumnIndex)).toInt()); + + if (state==RecordBase::State_Deleted) + continue; + + // check other columns (only relevant for a subset of the tables) + int dialogueTypeIndex = model.searchColumnIndex (Columns::ColumnId_DialogueType); + + if (dialogueTypeIndex!=-1) + { + int type = model.data (model.index (row, dialogueTypeIndex)).toInt(); + + if (type!=ESM::Dialogue::Topic && type!=ESM::Dialogue::Journal) + continue; + } + + result.push_back (*iter); + } + + return result; +} + +std::vector CSMWorld::CommandDispatcher::getRevertableRecords() const +{ + std::vector result; + + IdTable& model = dynamic_cast (*mDocument.getData().getTableModel (mId)); + + /// \todo Reverting temporarily disabled on tables that support reordering, because + /// revert logic currently can not handle reordering. + if (model.getFeatures() & IdTable::Feature_ReorderWithinTopic) + return result; + + int stateColumnIndex = model.findColumnIndex (Columns::ColumnId_Modification); + + for (std::vector::const_iterator iter (mSelection.begin()); + iter!=mSelection.end(); ++iter) + { + int row = model.getModelIndex (*iter, 0).row(); + + // check record state + RecordBase::State state = static_cast ( + model.data (model.index (row, stateColumnIndex)).toInt()); + + if (state==RecordBase::State_BaseOnly) + continue; + + result.push_back (*iter); + } + + return result; +} + +CSMWorld::CommandDispatcher::CommandDispatcher (CSMDoc::Document& document, + const CSMWorld::UniversalId& id, QObject *parent) +: QObject (parent), mDocument (document), mId (id), mLocked (false) +{} + +void CSMWorld::CommandDispatcher::setEditLock (bool locked) +{ + mLocked = locked; +} + +void CSMWorld::CommandDispatcher::setSelection (const std::vector& selection) +{ + mSelection = selection; + std::for_each (mSelection.begin(), mSelection.end(), Misc::StringUtils::toLower); + std::sort (mSelection.begin(), mSelection.end()); +} + +void CSMWorld::CommandDispatcher::setExtendedTypes (const std::vector& types) +{ + mExtendedTypes = types; +} + +bool CSMWorld::CommandDispatcher::canDelete() const +{ + if (mLocked) + return false; + + return getDeletableRecords().size()!=0; +} + +bool CSMWorld::CommandDispatcher::canRevert() const +{ + if (mLocked) + return false; + + return getRevertableRecords().size()!=0; +} + +std::vector CSMWorld::CommandDispatcher::getExtendedTypes() const +{ + std::vector tables; + + if (mId==UniversalId::Type_Cells) + { + tables.push_back (mId); + tables.push_back (UniversalId::Type_References); + /// \todo add other cell-specific types + } + + return tables; +} + +void CSMWorld::CommandDispatcher::executeDelete() +{ + if (mLocked) + return; + + std::vector rows = getDeletableRecords(); + + if (rows.empty()) + return; + + IdTable& model = dynamic_cast (*mDocument.getData().getTableModel (mId)); + + int columnIndex = model.findColumnIndex (Columns::ColumnId_Id); + + if (rows.size()>1) + mDocument.getUndoStack().beginMacro (tr ("Delete multiple records")); + + for (std::vector::const_iterator iter (rows.begin()); iter!=rows.end(); ++iter) + { + std::string id = model.data (model.getModelIndex (*iter, columnIndex)). + toString().toUtf8().constData(); + + mDocument.getUndoStack().push (new CSMWorld::DeleteCommand (model, id)); + } + + if (rows.size()>1) + mDocument.getUndoStack().endMacro(); +} + +void CSMWorld::CommandDispatcher::executeRevert() +{ + if (mLocked) + return; + + std::vector rows = getRevertableRecords(); + + if (rows.empty()) + return; + + IdTable& model = dynamic_cast (*mDocument.getData().getTableModel (mId)); + + int columnIndex = model.findColumnIndex (Columns::ColumnId_Id); + + if (rows.size()>1) + mDocument.getUndoStack().beginMacro (tr ("Revert multiple records")); + + for (std::vector::const_iterator iter (rows.begin()); iter!=rows.end(); ++iter) + { + std::string id = model.data (model.getModelIndex (*iter, columnIndex)). + toString().toUtf8().constData(); + + mDocument.getUndoStack().push (new CSMWorld::RevertCommand (model, id)); + } + + if (rows.size()>1) + mDocument.getUndoStack().endMacro(); +} + +void CSMWorld::CommandDispatcher::executeExtendedDelete() +{ + if (mExtendedTypes.size()>1) + mDocument.getUndoStack().beginMacro (tr ("Extended delete of multiple records")); + + for (std::vector::const_iterator iter (mExtendedTypes.begin()); + iter!=mExtendedTypes.end(); ++iter) + { + if (*iter==mId) + executeDelete(); + else if (*iter==UniversalId::Type_References) + { + IdTable& model = dynamic_cast ( + *mDocument.getData().getTableModel (*iter)); + + const RefCollection& collection = mDocument.getData().getReferences(); + + int size = collection.getSize(); + + for (int i=size-1; i>=0; --i) + { + const Record& record = collection.getRecord (i); + + if (record.mState==RecordBase::State_Deleted) + continue; + + if (!std::binary_search (mSelection.begin(), mSelection.end(), + Misc::StringUtils::lowerCase (record.get().mCell))) + continue; + + mDocument.getUndoStack().push ( + new CSMWorld::DeleteCommand (model, record.get().mId)); + } + } + } + + if (mExtendedTypes.size()>1) + mDocument.getUndoStack().endMacro(); +} + +void CSMWorld::CommandDispatcher::executeExtendedRevert() +{ + if (mExtendedTypes.size()>1) + mDocument.getUndoStack().beginMacro (tr ("Extended revert of multiple records")); + + for (std::vector::const_iterator iter (mExtendedTypes.begin()); + iter!=mExtendedTypes.end(); ++iter) + { + if (*iter==mId) + executeRevert(); + else if (*iter==UniversalId::Type_References) + { + IdTable& model = dynamic_cast ( + *mDocument.getData().getTableModel (*iter)); + + const RefCollection& collection = mDocument.getData().getReferences(); + + int size = collection.getSize(); + + for (int i=size-1; i>=0; --i) + { + const Record& record = collection.getRecord (i); + + if (!std::binary_search (mSelection.begin(), mSelection.end(), + Misc::StringUtils::lowerCase (record.get().mCell))) + continue; + + mDocument.getUndoStack().push ( + new CSMWorld::RevertCommand (model, record.get().mId)); + } + } + } + + if (mExtendedTypes.size()>1) + mDocument.getUndoStack().endMacro(); +} \ No newline at end of file diff --git a/apps/opencs/model/world/commanddispatcher.hpp b/apps/opencs/model/world/commanddispatcher.hpp new file mode 100644 index 000000000..50085b1a1 --- /dev/null +++ b/apps/opencs/model/world/commanddispatcher.hpp @@ -0,0 +1,69 @@ +#ifndef CSM_WOLRD_COMMANDDISPATCHER_H +#define CSM_WOLRD_COMMANDDISPATCHER_H + +#include + +#include + +#include "universalid.hpp" + +namespace CSMDoc +{ + class Document; +} + +namespace CSMWorld +{ + class CommandDispatcher : public QObject + { + Q_OBJECT + + bool mLocked; + CSMDoc::Document& mDocument; + UniversalId mId; + std::vector mSelection; + std::vector mExtendedTypes; + + std::vector getDeletableRecords() const; + + std::vector getRevertableRecords() const; + + public: + + CommandDispatcher (CSMDoc::Document& document, const CSMWorld::UniversalId& id, + QObject *parent = 0); + ///< \param id ID of the table the commands should operate on primarily. + + void setEditLock (bool locked); + + void setSelection (const std::vector& selection); + + void setExtendedTypes (const std::vector& types); + ///< Set record lists selected by the user for extended operations. + + bool canDelete() const; + + bool canRevert() const; + + /// Return IDs of the record collection that can also be affected when + /// operating on the record collection this dispatcher is used for. + /// + /// \note The returned collection contains the ID of the record collection this + /// dispatcher is used for. However if that record collection does not support + /// the extended mode, the returned vector will be empty instead. + std::vector getExtendedTypes() const; + + public slots: + + void executeDelete(); + + void executeRevert(); + + void executeExtendedDelete(); + + void executeExtendedRevert(); + + }; +} + +#endif diff --git a/apps/opencs/model/world/commands.cpp b/apps/opencs/model/world/commands.cpp index b60ffeb29..de0e9a4e5 100644 --- a/apps/opencs/model/world/commands.cpp +++ b/apps/opencs/model/world/commands.cpp @@ -10,13 +10,12 @@ CSMWorld::ModifyCommand::ModifyCommand (QAbstractItemModel& model, const QModelI const QVariant& new_, QUndoCommand* parent) : QUndoCommand (parent), mModel (model), mIndex (index), mNew (new_) { - mOld = mModel.data (mIndex, Qt::EditRole); - setText ("Modify " + mModel.headerData (mIndex.column(), Qt::Horizontal, Qt::DisplayRole).toString()); } void CSMWorld::ModifyCommand::redo() { + mOld = mModel.data (mIndex, Qt::EditRole); mModel.setData (mIndex, mNew); } @@ -25,6 +24,13 @@ void CSMWorld::ModifyCommand::undo() mModel.setData (mIndex, mOld); } + +void CSMWorld::CreateCommand::applyModifications() +{ + for (std::map::const_iterator iter (mValues.begin()); iter!=mValues.end(); ++iter) + mModel.setData (mModel.getModelIndex (mId, iter->first), iter->second); +} + CSMWorld::CreateCommand::CreateCommand (IdTable& model, const std::string& id, QUndoCommand* parent) : QUndoCommand (parent), mModel (model), mId (id), mType (UniversalId::Type_None) { @@ -44,9 +50,7 @@ void CSMWorld::CreateCommand::setType (UniversalId::Type type) void CSMWorld::CreateCommand::redo() { mModel.addRecord (mId, mType); - - for (std::map::const_iterator iter (mValues.begin()); iter!=mValues.end(); ++iter) - mModel.setData (mModel.getModelIndex (mId, iter->first), iter->second); + applyModifications(); } void CSMWorld::CreateCommand::undo() @@ -148,27 +152,22 @@ void CSMWorld::ReorderRowsCommand::undo() CSMWorld::CloneCommand::CloneCommand (CSMWorld::IdTable& model, const std::string& idOrigin, - const std::string& IdDestination, + const std::string& idDestination, const CSMWorld::UniversalId::Type type, - QUndoCommand* parent) : - QUndoCommand (parent), - mModel (model), - mIdOrigin (idOrigin), - mIdDestination (Misc::StringUtils::lowerCase (IdDestination)), - mType (type) + QUndoCommand* parent) +: CreateCommand (model, idDestination, parent), mIdOrigin (idOrigin) { - setText ( ("Clone record " + idOrigin + " to the " + IdDestination).c_str()); + setType (type); + setText ( ("Clone record " + idOrigin + " to the " + idDestination).c_str()); } void CSMWorld::CloneCommand::redo() { - mModel.cloneRecord (mIdOrigin, mIdDestination, mType); - - for (std::map::const_iterator iter (mValues.begin()); iter != mValues.end(); ++iter) - mModel.setData (mModel.getModelIndex (mIdDestination, iter->first), iter->second); + mModel.cloneRecord (mIdOrigin, mId, mType); + applyModifications(); } void CSMWorld::CloneCommand::undo() { - mModel.removeRow (mModel.getModelIndex (mIdDestination, 0).row()); + mModel.removeRow (mModel.getModelIndex (mId, 0).row()); } \ No newline at end of file diff --git a/apps/opencs/model/world/commands.hpp b/apps/opencs/model/world/commands.hpp index ec6350658..a15c071a8 100644 --- a/apps/opencs/model/world/commands.hpp +++ b/apps/opencs/model/world/commands.hpp @@ -39,40 +39,44 @@ namespace CSMWorld virtual void undo(); }; - class CloneCommand : public QUndoCommand + class CreateCommand : public QUndoCommand { + std::map mValues; + + protected: + IdTable& mModel; - std::string mIdOrigin; - std::string mIdDestination; + std::string mId; UniversalId::Type mType; - std::map mValues; + + protected: + + /// Apply modifications set via addValue. + void applyModifications(); public: - CloneCommand (IdTable& model, const std::string& idOrigin, - const std::string& IdDestination, - const UniversalId::Type type, - QUndoCommand* parent = 0); + CreateCommand (IdTable& model, const std::string& id, QUndoCommand *parent = 0); + + void setType (UniversalId::Type type); + + void addValue (int column, const QVariant& value); virtual void redo(); virtual void undo(); }; - class CreateCommand : public QUndoCommand + class CloneCommand : public CreateCommand { - IdTable& mModel; - std::string mId; - UniversalId::Type mType; - std::map mValues; + std::string mIdOrigin; public: - CreateCommand (IdTable& model, const std::string& id, QUndoCommand *parent = 0); - - void setType (UniversalId::Type type); - - void addValue (int column, const QVariant& value); + CloneCommand (IdTable& model, const std::string& idOrigin, + const std::string& IdDestination, + const UniversalId::Type type, + QUndoCommand* parent = 0); virtual void redo(); diff --git a/apps/opencs/model/world/data.cpp b/apps/opencs/model/world/data.cpp index ff33b4665..9cb0299c4 100644 --- a/apps/opencs/model/world/data.cpp +++ b/apps/opencs/model/world/data.cpp @@ -15,12 +15,15 @@ #include "columnimp.hpp" #include "regionmap.hpp" #include "columns.hpp" +#include "resourcesmanager.hpp" +#include "resourcetable.hpp" -void CSMWorld::Data::addModel (QAbstractItemModel *model, UniversalId::Type type1, - UniversalId::Type type2, bool update) +void CSMWorld::Data::addModel (QAbstractItemModel *model, UniversalId::Type type, bool update) { mModels.push_back (model); - mModelIndex.insert (std::make_pair (type1, model)); + mModelIndex.insert (std::make_pair (type, model)); + + UniversalId::Type type2 = UniversalId::getParentType (type); if (type2!=UniversalId::Type_None) mModelIndex.insert (std::make_pair (type2, model)); @@ -55,8 +58,9 @@ int CSMWorld::Data::count (RecordBase::State state, const CollectionBase& collec return number; } -CSMWorld::Data::Data (ToUTF8::FromType encoding) -: mEncoder (encoding), mRefs (mCells), mReader (0), mDialogue (0) +CSMWorld::Data::Data (ToUTF8::FromType encoding, const ResourcesManager& resourcesManager) +: mEncoder (encoding), mPathgrids (mCells), mRefs (mCells), + mResourcesManager (resourcesManager), mReader (0), mDialogue (0), mReaderIndex(0) { mGlobals.addColumn (new StringIdColumn); mGlobals.addColumn (new RecordStateColumn); @@ -67,7 +71,6 @@ CSMWorld::Data::Data (ToUTF8::FromType encoding) mGmsts.addColumn (new StringIdColumn); mGmsts.addColumn (new RecordStateColumn); mGmsts.addColumn (new FixedRecordTypeColumn (UniversalId::Type_Gmst)); - mGmsts.addColumn (new FixedRecordTypeColumn (UniversalId::Type_Gmst)); mGmsts.addColumn (new VarTypeColumn (ColumnBase::Display_GmstVarType)); mGmsts.addColumn (new VarValueColumn); @@ -101,7 +104,7 @@ CSMWorld::Data::Data (ToUTF8::FromType encoding) mFactions.addColumn (new AttributesColumn (0)); mFactions.addColumn (new AttributesColumn (1)); mFactions.addColumn (new HiddenColumn); - for (int i=0; i<6; ++i) + for (int i=0; i<7; ++i) mFactions.addColumn (new SkillsColumn (i)); mRaces.addColumn (new StringIdColumn); @@ -127,7 +130,7 @@ CSMWorld::Data::Data (ToUTF8::FromType encoding) mScripts.addColumn (new StringIdColumn); mScripts.addColumn (new RecordStateColumn); mScripts.addColumn (new FixedRecordTypeColumn (UniversalId::Type_Script)); - mScripts.addColumn (new ScriptColumn); + mScripts.addColumn (new ScriptColumn (ScriptColumn::Type_File)); mRegions.addColumn (new StringIdColumn); mRegions.addColumn (new RecordStateColumn); @@ -182,7 +185,7 @@ CSMWorld::Data::Data (ToUTF8::FromType encoding) mJournalInfos.addColumn (new StringIdColumn (true)); mJournalInfos.addColumn (new RecordStateColumn); - mJournalInfos.addColumn (new FixedRecordTypeColumn (UniversalId::Type_Journal)); + mJournalInfos.addColumn (new FixedRecordTypeColumn (UniversalId::Type_JournalInfo)); mJournalInfos.addColumn (new TopicColumn (true)); mJournalInfos.addColumn (new QuestStatusTypeColumn); mJournalInfos.addColumn (new QuestIndexColumn); @@ -196,6 +199,60 @@ CSMWorld::Data::Data (ToUTF8::FromType encoding) mCells.addColumn (new FlagColumn (Columns::ColumnId_InteriorWater, ESM::Cell::HasWater)); mCells.addColumn (new FlagColumn (Columns::ColumnId_InteriorSky, ESM::Cell::QuasiEx)); mCells.addColumn (new RegionColumn); + mCells.addColumn (new RefNumCounterColumn); + + mEnchantments.addColumn (new StringIdColumn); + mEnchantments.addColumn (new RecordStateColumn); + mEnchantments.addColumn (new FixedRecordTypeColumn (UniversalId::Type_Enchantment)); + mEnchantments.addColumn (new EnchantmentTypeColumn); + mEnchantments.addColumn (new CostColumn); + mEnchantments.addColumn (new ChargesColumn2); + mEnchantments.addColumn (new AutoCalcColumn); + + mBodyParts.addColumn (new StringIdColumn); + mBodyParts.addColumn (new RecordStateColumn); + mBodyParts.addColumn (new FixedRecordTypeColumn (UniversalId::Type_BodyPart)); + mBodyParts.addColumn (new BodyPartTypeColumn); + mBodyParts.addColumn (new VampireColumn); + mBodyParts.addColumn (new FlagColumn (Columns::ColumnId_Female, ESM::BodyPart::BPF_Female)); + mBodyParts.addColumn (new FlagColumn (Columns::ColumnId_Playable, ESM::BodyPart::BPF_NotPlayable, true)); + mBodyParts.addColumn (new MeshTypeColumn); + mBodyParts.addColumn (new ModelColumn); + mBodyParts.addColumn (new RaceColumn); + + mSoundGens.addColumn (new StringIdColumn); + mSoundGens.addColumn (new RecordStateColumn); + mSoundGens.addColumn (new FixedRecordTypeColumn (UniversalId::Type_SoundGen)); + mSoundGens.addColumn (new CreatureColumn); + mSoundGens.addColumn (new SoundColumn); + mSoundGens.addColumn (new SoundGeneratorTypeColumn); + + mMagicEffects.addColumn (new StringIdColumn); + mMagicEffects.addColumn (new RecordStateColumn); + mMagicEffects.addColumn (new FixedRecordTypeColumn (UniversalId::Type_MagicEffect)); + mMagicEffects.addColumn (new SchoolColumn); + mMagicEffects.addColumn (new BaseCostColumn); + mMagicEffects.addColumn (new EffectTextureColumn (Columns::ColumnId_Icon)); + mMagicEffects.addColumn (new EffectTextureColumn (Columns::ColumnId_Particle)); + mMagicEffects.addColumn (new EffectObjectColumn (Columns::ColumnId_CastingObject)); + mMagicEffects.addColumn (new EffectObjectColumn (Columns::ColumnId_HitObject)); + mMagicEffects.addColumn (new EffectObjectColumn (Columns::ColumnId_AreaObject)); + mMagicEffects.addColumn (new EffectObjectColumn (Columns::ColumnId_BoltObject)); + mMagicEffects.addColumn (new EffectSoundColumn (Columns::ColumnId_CastingSound)); + mMagicEffects.addColumn (new EffectSoundColumn (Columns::ColumnId_HitSound)); + mMagicEffects.addColumn (new EffectSoundColumn (Columns::ColumnId_AreaSound)); + mMagicEffects.addColumn (new EffectSoundColumn (Columns::ColumnId_BoltSound)); + mMagicEffects.addColumn (new FlagColumn ( + Columns::ColumnId_AllowSpellmaking, ESM::MagicEffect::AllowSpellmaking)); + mMagicEffects.addColumn (new FlagColumn ( + Columns::ColumnId_AllowEnchanting, ESM::MagicEffect::AllowEnchanting)); + mMagicEffects.addColumn (new FlagColumn ( + Columns::ColumnId_NegativeLight, ESM::MagicEffect::NegativeLight)); + mMagicEffects.addColumn (new DescriptionColumn); + + mPathgrids.addColumn (new StringIdColumn); + mPathgrids.addColumn (new RecordStateColumn); + mPathgrids.addColumn (new FixedRecordTypeColumn (UniversalId::Type_Pathgrid)); mRefs.addColumn (new StringIdColumn (true)); mRefs.addColumn (new RecordStateColumn); @@ -227,34 +284,66 @@ CSMWorld::Data::Data (ToUTF8::FromType encoding) mRefs.addColumn (new LockLevelColumn); mRefs.addColumn (new KeyColumn); mRefs.addColumn (new TrapColumn); - - mFilters.addColumn (new StringIdColumn); - mFilters.addColumn (new RecordStateColumn); - mFilters.addColumn (new FixedRecordTypeColumn (UniversalId::Type_Filter)); - mFilters.addColumn (new FilterColumn); - mFilters.addColumn (new DescriptionColumn); - mFilters.addColumn (new ScopeColumn); - - addModel (new IdTable (&mGlobals), UniversalId::Type_Globals, UniversalId::Type_Global); - addModel (new IdTable (&mGmsts), UniversalId::Type_Gmsts, UniversalId::Type_Gmst); - addModel (new IdTable (&mSkills), UniversalId::Type_Skills, UniversalId::Type_Skill, false); - addModel (new IdTable (&mClasses), UniversalId::Type_Classes, UniversalId::Type_Class); - addModel (new IdTable (&mFactions), UniversalId::Type_Factions, UniversalId::Type_Faction); - addModel (new IdTable (&mRaces), UniversalId::Type_Races, UniversalId::Type_Race); - addModel (new IdTable (&mSounds), UniversalId::Type_Sounds, UniversalId::Type_Sound); - addModel (new IdTable (&mScripts), UniversalId::Type_Scripts, UniversalId::Type_Script); - addModel (new IdTable (&mRegions), UniversalId::Type_Regions, UniversalId::Type_Region); - addModel (new IdTable (&mBirthsigns), UniversalId::Type_Birthsigns, UniversalId::Type_Birthsign); - addModel (new IdTable (&mSpells), UniversalId::Type_Spells, UniversalId::Type_Spell); - addModel (new IdTable (&mTopics), UniversalId::Type_Topics, UniversalId::Type_Topic); - addModel (new IdTable (&mJournals), UniversalId::Type_Journals, UniversalId::Type_Journal); - addModel (new IdTable (&mTopicInfos, IdTable::Reordering_WithinTopic), UniversalId::Type_TopicInfos, UniversalId::Type_TopicInfo); - addModel (new IdTable (&mJournalInfos, IdTable::Reordering_WithinTopic), UniversalId::Type_JournalInfos, UniversalId::Type_JournalInfo); - addModel (new IdTable (&mCells, IdTable::Reordering_None, IdTable::Viewing_Id), UniversalId::Type_Cells, UniversalId::Type_Cell); - addModel (new IdTable (&mReferenceables, IdTable::Reordering_None, IdTable::Viewing_None, true), - UniversalId::Type_Referenceables, UniversalId::Type_Referenceable); - addModel (new IdTable (&mRefs, IdTable::Reordering_None, IdTable::Viewing_Cell, true), UniversalId::Type_References, UniversalId::Type_Reference, false); - addModel (new IdTable (&mFilters), UniversalId::Type_Filters, UniversalId::Type_Filter, false); + mRefs.addColumn (new OwnerGlobalColumn); + mRefs.addColumn (new RefNumColumn); + + mFilters.addColumn (new StringIdColumn); + mFilters.addColumn (new RecordStateColumn); + mFilters.addColumn (new FixedRecordTypeColumn (UniversalId::Type_Filter)); + mFilters.addColumn (new FilterColumn); + mFilters.addColumn (new DescriptionColumn); + + mDebugProfiles.addColumn (new StringIdColumn); + mDebugProfiles.addColumn (new RecordStateColumn); + mDebugProfiles.addColumn (new FixedRecordTypeColumn (UniversalId::Type_DebugProfile)); + mDebugProfiles.addColumn (new FlagColumn2 ( + Columns::ColumnId_DefaultProfile, ESM::DebugProfile::Flag_Default)); + mDebugProfiles.addColumn (new FlagColumn2 ( + Columns::ColumnId_BypassNewGame, ESM::DebugProfile::Flag_BypassNewGame)); + mDebugProfiles.addColumn (new FlagColumn2 ( + Columns::ColumnId_GlobalProfile, ESM::DebugProfile::Flag_Global)); + mDebugProfiles.addColumn (new DescriptionColumn); + mDebugProfiles.addColumn (new ScriptColumn ( + ScriptColumn::Type_Lines)); + + addModel (new IdTable (&mGlobals), UniversalId::Type_Global); + addModel (new IdTable (&mGmsts), UniversalId::Type_Gmst); + addModel (new IdTable (&mSkills), UniversalId::Type_Skill); + addModel (new IdTable (&mClasses), UniversalId::Type_Class); + addModel (new IdTable (&mFactions), UniversalId::Type_Faction); + addModel (new IdTable (&mRaces), UniversalId::Type_Race); + addModel (new IdTable (&mSounds), UniversalId::Type_Sound); + addModel (new IdTable (&mScripts), UniversalId::Type_Script); + addModel (new IdTable (&mRegions), UniversalId::Type_Region); + addModel (new IdTable (&mBirthsigns), UniversalId::Type_Birthsign); + addModel (new IdTable (&mSpells), UniversalId::Type_Spell); + addModel (new IdTable (&mTopics), UniversalId::Type_Topic); + addModel (new IdTable (&mJournals), UniversalId::Type_Journal); + addModel (new IdTable (&mTopicInfos, IdTable::Feature_ReorderWithinTopic), UniversalId::Type_TopicInfo); + addModel (new IdTable (&mJournalInfos, IdTable::Feature_ReorderWithinTopic), UniversalId::Type_JournalInfo); + addModel (new IdTable (&mCells, IdTable::Feature_ViewId), UniversalId::Type_Cell); + addModel (new IdTable (&mEnchantments), UniversalId::Type_Enchantment); + addModel (new IdTable (&mBodyParts), UniversalId::Type_BodyPart); + addModel (new IdTable (&mSoundGens), UniversalId::Type_SoundGen); + addModel (new IdTable (&mMagicEffects), UniversalId::Type_MagicEffect); + addModel (new IdTable (&mPathgrids), UniversalId::Type_Pathgrid); + addModel (new IdTable (&mReferenceables, IdTable::Feature_Preview), + UniversalId::Type_Referenceable); + addModel (new IdTable (&mRefs, IdTable::Feature_ViewCell | IdTable::Feature_Preview), UniversalId::Type_Reference); + addModel (new IdTable (&mFilters), UniversalId::Type_Filter); + addModel (new IdTable (&mDebugProfiles), UniversalId::Type_DebugProfile); + addModel (new ResourceTable (&mResourcesManager.get (UniversalId::Type_Meshes)), + UniversalId::Type_Mesh); + addModel (new ResourceTable (&mResourcesManager.get (UniversalId::Type_Icons)), + UniversalId::Type_Icon); + addModel (new ResourceTable (&mResourcesManager.get (UniversalId::Type_Musics)), + UniversalId::Type_Music); + addModel (new ResourceTable (&mResourcesManager.get (UniversalId::Type_SoundsRes)), + UniversalId::Type_SoundRes); + addModel (new ResourceTable (&mResourcesManager.get (UniversalId::Type_Textures)), + UniversalId::Type_Texture); + addModel (new ResourceTable (&mResourcesManager.get (UniversalId::Type_Videos)), + UniversalId::Type_Video); } CSMWorld::Data::~Data() @@ -446,16 +535,91 @@ CSMWorld::RefCollection& CSMWorld::Data::getReferences() return mRefs; } -const CSMWorld::IdCollection& CSMWorld::Data::getFilters() const +const CSMWorld::IdCollection& CSMWorld::Data::getFilters() const { return mFilters; } -CSMWorld::IdCollection& CSMWorld::Data::getFilters() +CSMWorld::IdCollection& CSMWorld::Data::getFilters() { return mFilters; } +const CSMWorld::IdCollection& CSMWorld::Data::getEnchantments() const +{ + return mEnchantments; +} + +CSMWorld::IdCollection& CSMWorld::Data::getEnchantments() +{ + return mEnchantments; +} + +const CSMWorld::IdCollection& CSMWorld::Data::getBodyParts() const +{ + return mBodyParts; +} + +CSMWorld::IdCollection& CSMWorld::Data::getBodyParts() +{ + return mBodyParts; +} + +const CSMWorld::IdCollection& CSMWorld::Data::getDebugProfiles() const +{ + return mDebugProfiles; +} + +CSMWorld::IdCollection& CSMWorld::Data::getDebugProfiles() +{ + return mDebugProfiles; +} + +const CSMWorld::IdCollection& CSMWorld::Data::getLand() const +{ + return mLand; +} + +const CSMWorld::IdCollection& CSMWorld::Data::getLandTextures() const +{ + return mLandTextures; +} + +const CSMWorld::IdCollection& CSMWorld::Data::getSoundGens() const +{ + return mSoundGens; +} + +CSMWorld::IdCollection& CSMWorld::Data::getSoundGens() +{ + return mSoundGens; +} + +const CSMWorld::IdCollection& CSMWorld::Data::getMagicEffects() const +{ + return mMagicEffects; +} + +CSMWorld::IdCollection& CSMWorld::Data::getMagicEffects() +{ + return mMagicEffects; +} + +const CSMWorld::SubCellCollection& CSMWorld::Data::getPathgrids() const +{ + return mPathgrids; +} + +CSMWorld::SubCellCollection& CSMWorld::Data::getPathgrids() +{ + return mPathgrids; +} + +const CSMWorld::Resources& CSMWorld::Data::getResources (const UniversalId& id) const +{ + return mResourcesManager.get (id.getType()); +} + QAbstractItemModel *CSMWorld::Data::getTableModel (const CSMWorld::UniversalId& id) { std::map::iterator iter = mModelIndex.find (id.getType()); @@ -469,8 +633,7 @@ QAbstractItemModel *CSMWorld::Data::getTableModel (const CSMWorld::UniversalId& if (id.getType()==UniversalId::Type_RegionMap) { RegionMap *table = 0; - addModel (table = new RegionMap (*this), UniversalId::Type_RegionMap, - UniversalId::Type_None, false); + addModel (table = new RegionMap (*this), UniversalId::Type_RegionMap, false); return table; } throw std::logic_error ("No table model available for " + id.toString()); @@ -486,12 +649,17 @@ void CSMWorld::Data::merge() int CSMWorld::Data::startLoading (const boost::filesystem::path& path, bool base, bool project) { - delete mReader; + // Don't delete the Reader yet. Some record types store a reference to the Reader to handle on-demand loading + boost::shared_ptr ptr(mReader); + mReaders.push_back(ptr); mReader = 0; + mDialogue = 0; + mRefLoadCache.clear(); mReader = new ESM::ESMReader; mReader->setEncoder (&mEncoder); + mReader->setIndex(mReaderIndex++); mReader->open (path.string()); mBase = base; @@ -510,15 +678,21 @@ bool CSMWorld::Data::continueLoading (CSMDoc::Stage::Messages& messages) if (!mReader->hasMoreRecs()) { - delete mReader; + // Don't delete the Reader yet. Some record types store a reference to the Reader to handle on-demand loading + boost::shared_ptr ptr(mReader); + mReaders.push_back(ptr); mReader = 0; + mDialogue = 0; + mRefLoadCache.clear(); return true; } ESM::NAME n = mReader->getRecName(); mReader->getRecHeader(); + bool unhandledRecord = false; + switch (n.val) { case ESM::REC_GLOB: mGlobals.load (*mReader, mBase); break; @@ -532,11 +706,22 @@ bool CSMWorld::Data::continueLoading (CSMDoc::Stage::Messages& messages) case ESM::REC_REGN: mRegions.load (*mReader, mBase); break; case ESM::REC_BSGN: mBirthsigns.load (*mReader, mBase); break; case ESM::REC_SPEL: mSpells.load (*mReader, mBase); break; + case ESM::REC_ENCH: mEnchantments.load (*mReader, mBase); break; + case ESM::REC_BODY: mBodyParts.load (*mReader, mBase); break; + case ESM::REC_SNDG: mSoundGens.load (*mReader, mBase); break; + case ESM::REC_MGEF: mMagicEffects.load (*mReader, mBase); break; + case ESM::REC_PGRD: mPathgrids.load (*mReader, mBase); break; + + case ESM::REC_LTEX: mLandTextures.load (*mReader, mBase); break; + case ESM::REC_LAND: mLand.load(*mReader, mBase); break; case ESM::REC_CELL: + { mCells.load (*mReader, mBase); - mRefs.load (*mReader, mCells.getSize()-1, mBase); + std::string cellId = Misc::StringUtils::lowerCase (mCells.getId (mCells.getSize()-1)); + mRefs.load (*mReader, mCells.getSize()-1, mBase, mRefLoadCache[cellId], messages); break; + } case ESM::REC_ACTI: mReferenceables.load (*mReader, mBase, UniversalId::Type_Activator); break; case ESM::REC_ALCH: mReferenceables.load (*mReader, mBase, UniversalId::Type_Potion); break; @@ -624,23 +809,37 @@ bool CSMWorld::Data::continueLoading (CSMDoc::Stage::Messages& messages) case ESM::REC_FILT: - if (mProject) + if (!mProject) + { + unhandledRecord = true; + break; + } + + mFilters.load (*mReader, mBase); + break; + + case ESM::REC_DBGP: + + if (!mProject) { - mFilters.load (*mReader, mBase); - mFilters.setData (mFilters.getSize()-1, - mFilters.findColumnIndex (CSMWorld::Columns::ColumnId_Scope), - static_cast (CSMFilter::Filter::Scope_Project)); + unhandledRecord = true; break; } - // fall through (filter record in a content file is an error with format 0) + mDebugProfiles.load (*mReader, mBase); + break; default: - messages.push_back (std::make_pair (UniversalId::Type_None, - "Unsupported record type: " + n.toString())); + unhandledRecord = true; + } + + if (unhandledRecord) + { + messages.push_back (std::make_pair (UniversalId::Type_None, + "Unsupported record type: " + n.toString())); - mReader->skipRecord(); + mReader->skipRecord(); } return false; @@ -663,6 +862,10 @@ bool CSMWorld::Data::hasId (const std::string& id) const getTopics().searchId (id)!=-1 || getJournals().searchId (id)!=-1 || getCells().searchId (id)!=-1 || + getEnchantments().searchId (id)!=-1 || + getBodyParts().searchId (id)!=-1 || + getSoundGens().searchId (id)!=-1 || + getMagicEffects().searchId (id)!=-1 || getReferenceables().searchId (id)!=-1; } @@ -681,7 +884,14 @@ int CSMWorld::Data::count (RecordBase::State state) const count (state, mBirthsigns) + count (state, mSpells) + count (state, mCells) + - count (state, mReferenceables); + count (state, mEnchantments) + + count (state, mBodyParts) + + count (state, mLand) + + count (state, mLandTextures) + + count (state, mSoundGens) + + count (state, mMagicEffects) + + count (state, mReferenceables) + + count (state, mPathgrids); } void CSMWorld::Data::setDescription (const std::string& description) @@ -721,6 +931,10 @@ std::vector CSMWorld::Data::getIds (bool listDeleted) const appendIds (ids, mTopics, listDeleted); appendIds (ids, mJournals, listDeleted); appendIds (ids, mCells, listDeleted); + appendIds (ids, mEnchantments, listDeleted); + appendIds (ids, mBodyParts, listDeleted); + appendIds (ids, mSoundGens, listDeleted); + appendIds (ids, mMagicEffects, listDeleted); appendIds (ids, mReferenceables, listDeleted); std::sort (ids.begin(), ids.end()); @@ -737,4 +951,4 @@ void CSMWorld::Data::dataChanged (const QModelIndex& topLeft, const QModelIndex& void CSMWorld::Data::rowsChanged (const QModelIndex& parent, int start, int end) { emit idListChanged(); -} \ No newline at end of file +} diff --git a/apps/opencs/model/world/data.hpp b/apps/opencs/model/world/data.hpp index ab247b6a3..37d4d4b8a 100644 --- a/apps/opencs/model/world/data.hpp +++ b/apps/opencs/model/world/data.hpp @@ -21,19 +21,27 @@ #include #include #include +#include +#include +#include +#include +#include +#include #include -#include "../filter/filter.hpp" - #include "../doc/stage.hpp" #include "idcollection.hpp" #include "universalid.hpp" #include "cell.hpp" +#include "land.hpp" +#include "landtexture.hpp" #include "refidcollection.hpp" #include "refcollection.hpp" #include "infocollection.hpp" +#include "pathgrid.hpp" +#include "subcellcollection.hpp" class QAbstractItemModel; @@ -45,6 +53,9 @@ namespace ESM namespace CSMWorld { + class ResourcesManager; + class Resources; + class Data : public QObject { Q_OBJECT @@ -63,12 +74,21 @@ namespace CSMWorld IdCollection mSpells; IdCollection mTopics; IdCollection mJournals; + IdCollection mEnchantments; + IdCollection mBodyParts; + IdCollection mMagicEffects; + SubCellCollection mPathgrids; + IdCollection mDebugProfiles; + IdCollection mSoundGens; InfoCollection mTopicInfos; InfoCollection mJournalInfos; IdCollection mCells; + IdCollection mLandTextures; + IdCollection mLand; RefIdCollection mReferenceables; RefCollection mRefs; - IdCollection mFilters; + IdCollection mFilters; + const ResourcesManager& mResourcesManager; std::vector mModels; std::map mModelIndex; std::string mAuthor; @@ -77,13 +97,17 @@ namespace CSMWorld const ESM::Dialogue *mDialogue; // last loaded dialogue bool mBase; bool mProject; + std::map > mRefLoadCache; + int mReaderIndex; + + std::vector > mReaders; // not implemented Data (const Data&); Data& operator= (const Data&); - void addModel (QAbstractItemModel *model, UniversalId::Type type1, - UniversalId::Type type2 = UniversalId::Type_None, bool update = true); + void addModel (QAbstractItemModel *model, UniversalId::Type type, + bool update = true); static void appendIds (std::vector& ids, const CollectionBase& collection, bool listDeleted); @@ -93,7 +117,7 @@ namespace CSMWorld public: - Data (ToUTF8::FromType encoding); + Data (ToUTF8::FromType encoding, const ResourcesManager& resourcesManager); virtual ~Data(); @@ -169,9 +193,40 @@ namespace CSMWorld RefCollection& getReferences(); - const IdCollection& getFilters() const; + const IdCollection& getFilters() const; + + IdCollection& getFilters(); + + const IdCollection& getEnchantments() const; + + IdCollection& getEnchantments(); + + const IdCollection& getBodyParts() const; + + IdCollection& getBodyParts(); + + const IdCollection& getDebugProfiles() const; + + IdCollection& getDebugProfiles(); + + const IdCollection& getLand() const; + + const IdCollection& getLandTextures() const; + + const IdCollection& getSoundGens() const; + + IdCollection& getSoundGens(); + + const IdCollection& getMagicEffects() const; + + IdCollection& getMagicEffects(); + + const SubCellCollection& getPathgrids() const; + + SubCellCollection& getPathgrids(); - IdCollection& getFilters(); + /// Throws an exception, if \a id does not match a resources list. + const Resources& getResources (const UniversalId& id) const; QAbstractItemModel *getTableModel (const UniversalId& id); ///< If no table model is available for \a id, an exception is thrown. @@ -222,4 +277,4 @@ namespace CSMWorld }; } -#endif \ No newline at end of file +#endif diff --git a/apps/opencs/model/world/idcollection.hpp b/apps/opencs/model/world/idcollection.hpp index a7b37be5b..0129ba3d8 100644 --- a/apps/opencs/model/world/idcollection.hpp +++ b/apps/opencs/model/world/idcollection.hpp @@ -11,11 +11,16 @@ namespace CSMWorld template > class IdCollection : public Collection { + virtual void loadRecord (ESXRecordT& record, ESM::ESMReader& reader); + public: void load (ESM::ESMReader& reader, bool base); - void load (const ESXRecordT& record, bool base); + /// \param index Index at which the record can be found. + /// Special values: -2 index unknown, -1 record does not exist yet and therefore + /// does not have an index + void load (const ESXRecordT& record, bool base, int index = -2); bool tryDelete (const std::string& id); ///< Try deleting \a id. If the id does not exist or can't be deleted the call is ignored. @@ -23,6 +28,13 @@ namespace CSMWorld /// \return Has the ID been deleted? }; + template + void IdCollection::loadRecord (ESXRecordT& record, + ESM::ESMReader& reader) + { + record.load (reader); + } + template void IdCollection::load (ESM::ESMReader& reader, bool base) { @@ -56,17 +68,36 @@ namespace CSMWorld else { ESXRecordT record; - IdAccessorT().getId (record) = id; - record.load (reader); - load (record, base); + int index = this->searchId (id); + + if (index==-1) + IdAccessorT().getId (record) = id; + else + { + record = this->getRecord (index).get(); + } + + loadRecord (record, reader); + + if (index==-1) + { + std::string newId = IdAccessorT().getId(record); + int newIndex = this->searchId(newId); + if (newIndex != -1 && id != newId) + index = newIndex; + } + + load (record, base, index); } } template - void IdCollection::load (const ESXRecordT& record, bool base) + void IdCollection::load (const ESXRecordT& record, bool base, + int index) { - int index = this->searchId (IdAccessorT().getId (record)); + if (index==-2) + index = this->searchId (IdAccessorT().getId (record)); if (index==-1) { diff --git a/apps/opencs/model/world/idtable.cpp b/apps/opencs/model/world/idtable.cpp index 50998c36f..c68b71789 100644 --- a/apps/opencs/model/world/idtable.cpp +++ b/apps/opencs/model/world/idtable.cpp @@ -4,9 +4,8 @@ #include "collectionbase.hpp" #include "columnbase.hpp" -CSMWorld::IdTable::IdTable (CollectionBase *idCollection, Reordering reordering, - Viewing viewing, bool preview) -: mIdCollection (idCollection), mReordering (reordering), mViewing (viewing), mPreview (preview) +CSMWorld::IdTable::IdTable (CollectionBase *idCollection, unsigned int features) +: IdTableBase (features), mIdCollection (idCollection) {} CSMWorld::IdTable::~IdTable() @@ -30,7 +29,7 @@ int CSMWorld::IdTable::columnCount (const QModelIndex & parent) const QVariant CSMWorld::IdTable::data (const QModelIndex & index, int role) const { - if (role!=Qt::DisplayRole && role!=Qt::EditRole) + if ((role!=Qt::DisplayRole && role!=Qt::EditRole) || index.row() < 0 || index.column() < 0) return QVariant(); if (role==Qt::EditRole && !mIdCollection->getColumn (index.column()).isEditable()) @@ -186,27 +185,12 @@ void CSMWorld::IdTable::reorderRows (int baseIndex, const std::vector& newO index (baseIndex+newOrder.size()-1, mIdCollection->getColumns()-1)); } -CSMWorld::IdTable::Reordering CSMWorld::IdTable::getReordering() const -{ - return mReordering; -} - -CSMWorld::IdTable::Viewing CSMWorld::IdTable::getViewing() const -{ - return mViewing; -} - -bool CSMWorld::IdTable::hasPreview() const -{ - return mPreview; -} - std::pair CSMWorld::IdTable::view (int row) const { std::string id; std::string hint; - if (mViewing==Viewing_Cell) + if (getFeatures() & Feature_ViewCell) { int cellColumn = mIdCollection->searchColumnIndex (Columns::ColumnId_Cell); int idColumn = mIdCollection->searchColumnIndex (Columns::ColumnId_Id); @@ -217,7 +201,7 @@ std::pair CSMWorld::IdTable::view (int row) hint = "r:" + std::string (mIdCollection->getData (row, idColumn).toString().toUtf8().constData()); } } - else if (mViewing==Viewing_Id) + else if (getFeatures() & Feature_ViewId) { int column = mIdCollection->searchColumnIndex (Columns::ColumnId_Id); @@ -237,7 +221,12 @@ std::pair CSMWorld::IdTable::view (int row) return std::make_pair (UniversalId (UniversalId::Type_Scene, id), hint); } +bool CSMWorld::IdTable::isDeleted (const std::string& id) const +{ + return getRecord (id).isDeleted(); +} + int CSMWorld::IdTable::getColumnId(int column) const { return mIdCollection->getColumn(column).getId(); -} \ No newline at end of file +} diff --git a/apps/opencs/model/world/idtable.hpp b/apps/opencs/model/world/idtable.hpp index 8b5462825..707d7133b 100644 --- a/apps/opencs/model/world/idtable.hpp +++ b/apps/opencs/model/world/idtable.hpp @@ -3,8 +3,7 @@ #include -#include - +#include "idtablebase.hpp" #include "universalid.hpp" #include "columns.hpp" @@ -13,33 +12,13 @@ namespace CSMWorld class CollectionBase; class RecordBase; - class IdTable : public QAbstractItemModel + class IdTable : public IdTableBase { Q_OBJECT - public: - - enum Reordering - { - Reordering_None, - Reordering_WithinTopic - }; - - enum Viewing - { - Viewing_None, - Viewing_Id, // use ID column to generate view request (ID is transformed into - // worldspace and original ID is passed as hint with c: prefix) - Viewing_Cell // use cell column to generate view request (cell ID is transformed - // into worldspace and record ID is passed as hint with r: prefix) - }; - private: CollectionBase *mIdCollection; - Reordering mReordering; - Viewing mViewing; - bool mPreview; // not implemented IdTable (const IdTable&); @@ -47,8 +26,7 @@ namespace CSMWorld public: - IdTable (CollectionBase *idCollection, Reordering reordering = Reordering_None, - Viewing viewing = Viewing_None, bool preview = false); + IdTable (CollectionBase *idCollection, unsigned int features = 0); ///< The ownership of \a idCollection is not transferred. virtual ~IdTable(); @@ -79,17 +57,17 @@ namespace CSMWorld const std::string& destination, UniversalId::Type type = UniversalId::Type_None); - QModelIndex getModelIndex (const std::string& id, int column) const; + virtual QModelIndex getModelIndex (const std::string& id, int column) const; void setRecord (const std::string& id, const RecordBase& record); ///< Add record or overwrite existing recrod. const RecordBase& getRecord (const std::string& id) const; - int searchColumnIndex (Columns::ColumnId id) const; + virtual int searchColumnIndex (Columns::ColumnId id) const; ///< Return index of column with the given \a id. If no such column exists, -1 is returned. - int findColumnIndex (Columns::ColumnId id) const; + virtual int findColumnIndex (Columns::ColumnId id) const; ///< Return index of column with the given \a id. If no such column exists, an exception is /// thrown. @@ -97,16 +75,13 @@ namespace CSMWorld ///< Reorder the rows [baseIndex, baseIndex+newOrder.size()) according to the indices /// given in \a newOrder (baseIndex+newOrder[0] specifies the new index of row baseIndex). - Reordering getReordering() const; - - Viewing getViewing() const; - - bool hasPreview() const; - - std::pair view (int row) const; + virtual std::pair view (int row) const; ///< Return the UniversalId and the hint for viewing \a row. If viewing is not /// supported by this table, return (UniversalId::Type_None, ""). + /// Is \a id flagged as deleted? + virtual bool isDeleted (const std::string& id) const; + int getColumnId(int column) const; }; } diff --git a/apps/opencs/model/world/idtablebase.cpp b/apps/opencs/model/world/idtablebase.cpp new file mode 100644 index 000000000..31d8d461e --- /dev/null +++ b/apps/opencs/model/world/idtablebase.cpp @@ -0,0 +1,9 @@ + +#include "idtablebase.hpp" + +CSMWorld::IdTableBase::IdTableBase (unsigned int features) : mFeatures (features) {} + +unsigned int CSMWorld::IdTableBase::getFeatures() const +{ + return mFeatures; +} \ No newline at end of file diff --git a/apps/opencs/model/world/idtablebase.hpp b/apps/opencs/model/world/idtablebase.hpp new file mode 100644 index 000000000..ef5a9c42e --- /dev/null +++ b/apps/opencs/model/world/idtablebase.hpp @@ -0,0 +1,67 @@ +#ifndef CSM_WOLRD_IDTABLEBASE_H +#define CSM_WOLRD_IDTABLEBASE_H + +#include + +#include "columns.hpp" + +namespace CSMWorld +{ + class UniversalId; + + class IdTableBase : public QAbstractItemModel + { + Q_OBJECT + + public: + + enum Features + { + Feature_ReorderWithinTopic = 1, + + /// Use ID column to generate view request (ID is transformed into + /// worldspace and original ID is passed as hint with c: prefix). + Feature_ViewId = 2, + + /// Use cell column to generate view request (cell ID is transformed + /// into worldspace and record ID is passed as hint with r: prefix). + Feature_ViewCell = 4, + + Feature_View = Feature_ViewId | Feature_ViewCell, + + Feature_Preview = 8, + + /// Table can not be modified through ordinary means. + Feature_Constant = 16 + }; + + private: + + unsigned int mFeatures; + + public: + + IdTableBase (unsigned int features); + + virtual QModelIndex getModelIndex (const std::string& id, int column) const = 0; + + /// Return index of column with the given \a id. If no such column exists, -1 is + /// returned. + virtual int searchColumnIndex (Columns::ColumnId id) const = 0; + + /// Return index of column with the given \a id. If no such column exists, an + /// exception is thrown. + virtual int findColumnIndex (Columns::ColumnId id) const = 0; + + /// Return the UniversalId and the hint for viewing \a row. If viewing is not + /// supported by this table, return (UniversalId::Type_None, ""). + virtual std::pair view (int row) const = 0; + + /// Is \a id flagged as deleted? + virtual bool isDeleted (const std::string& id) const = 0; + + unsigned int getFeatures() const; + }; +} + +#endif diff --git a/apps/opencs/model/world/idtableproxymodel.cpp b/apps/opencs/model/world/idtableproxymodel.cpp index f51b7f818..93c1749c6 100644 --- a/apps/opencs/model/world/idtableproxymodel.cpp +++ b/apps/opencs/model/world/idtableproxymodel.cpp @@ -3,7 +3,7 @@ #include -#include "idtable.hpp" +#include "idtablebase.hpp" void CSMWorld::IdTableProxyModel::updateColumnMap() { @@ -13,7 +13,7 @@ void CSMWorld::IdTableProxyModel::updateColumnMap() { std::vector columns = mFilter->getReferencedColumns(); - const IdTable& table = dynamic_cast (*sourceModel()); + const IdTableBase& table = dynamic_cast (*sourceModel()); for (std::vector::const_iterator iter (columns.begin()); iter!=columns.end(); ++iter) mColumnMap.insert (std::make_pair (*iter, @@ -28,7 +28,7 @@ bool CSMWorld::IdTableProxyModel::filterAcceptsRow (int sourceRow, const QModelI return true; return mFilter->test ( - dynamic_cast (*sourceModel()), sourceRow, mColumnMap); + dynamic_cast (*sourceModel()), sourceRow, mColumnMap); } CSMWorld::IdTableProxyModel::IdTableProxyModel (QObject *parent) @@ -39,7 +39,7 @@ CSMWorld::IdTableProxyModel::IdTableProxyModel (QObject *parent) QModelIndex CSMWorld::IdTableProxyModel::getModelIndex (const std::string& id, int column) const { - return mapFromSource (dynamic_cast (*sourceModel()).getModelIndex (id, column)); + return mapFromSource (dynamic_cast (*sourceModel()).getModelIndex (id, column)); } void CSMWorld::IdTableProxyModel::setFilter (const boost::shared_ptr& filter) @@ -47,4 +47,9 @@ void CSMWorld::IdTableProxyModel::setFilter (const boost::shared_ptr& filter); + + protected: + + bool lessThan(const QModelIndex &left, const QModelIndex &right) const; }; } -#endif \ No newline at end of file +#endif diff --git a/apps/opencs/model/world/land.cpp b/apps/opencs/model/world/land.cpp new file mode 100644 index 000000000..119e18761 --- /dev/null +++ b/apps/opencs/model/world/land.cpp @@ -0,0 +1,28 @@ +#include "land.hpp" + +#include + +namespace CSMWorld +{ + + Land::Land() + { + mLand.reset(new ESM::Land()); + } + + void Land::load(ESM::ESMReader &esm) + { + mLand->load(esm); + + std::ostringstream stream; + stream << "#" << mLand->mX << " " << mLand->mY; + + mId = stream.str(); + } + + void Land::blank() + { + /// \todo + } + +} diff --git a/apps/opencs/model/world/land.hpp b/apps/opencs/model/world/land.hpp new file mode 100644 index 000000000..e97a2d7dd --- /dev/null +++ b/apps/opencs/model/world/land.hpp @@ -0,0 +1,29 @@ +#ifndef CSM_WORLD_LAND_H +#define CSM_WORLD_LAND_H + +#include +#include +#include + +namespace CSMWorld +{ + /// \brief Wrapper for Land record. Encodes X and Y cell index in the ID. + /// + /// \todo Add worldspace support to the Land record. + /// \todo Add a proper copy constructor (currently worked around using shared_ptr) + struct Land + { + Land(); + + boost::shared_ptr mLand; + + std::string mId; + + /// Loads the metadata and ID + void load (ESM::ESMReader &esm); + + void blank(); + }; +} + +#endif diff --git a/apps/opencs/model/world/landtexture.cpp b/apps/opencs/model/world/landtexture.cpp new file mode 100644 index 000000000..4725866a5 --- /dev/null +++ b/apps/opencs/model/world/landtexture.cpp @@ -0,0 +1,21 @@ +#include "landtexture.hpp" + +#include + +namespace CSMWorld +{ + + void LandTexture::load(ESM::ESMReader &esm) + { + ESM::LandTexture::load(esm); + + int plugin = esm.getIndex(); + + std::ostringstream stream; + + stream << mIndex << "_" << plugin; + + mId = stream.str(); + } + +} diff --git a/apps/opencs/model/world/landtexture.hpp b/apps/opencs/model/world/landtexture.hpp new file mode 100644 index 000000000..b13903186 --- /dev/null +++ b/apps/opencs/model/world/landtexture.hpp @@ -0,0 +1,22 @@ +#ifndef CSM_WORLD_LANDTEXTURE_H +#define CSM_WORLD_LANDTEXTURE_H + +#include + +#include + +namespace CSMWorld +{ + /// \brief Wrapper for LandTexture record. Encodes mIndex and the plugin index (obtained from ESMReader) + /// in the ID. + /// + /// \attention The mId field of the ESM::LandTexture struct is not used. + struct LandTexture : public ESM::LandTexture + { + std::string mId; + + void load (ESM::ESMReader &esm); + }; +} + +#endif diff --git a/apps/opencs/model/world/pathgrid.cpp b/apps/opencs/model/world/pathgrid.cpp new file mode 100644 index 000000000..1c82585df --- /dev/null +++ b/apps/opencs/model/world/pathgrid.cpp @@ -0,0 +1,35 @@ + +#include "pathgrid.hpp" + +#include + +void CSMWorld::Pathgrid::load (ESM::ESMReader &esm, const IdCollection& cells) +{ + load (esm); + + // correct ID + if (!mId.empty() && mId[0]!='#' && cells.searchId (mId)==-1) + { + std::ostringstream stream; + + stream << "#" << mData.mX << " " << mData.mY; + + mId = stream.str(); + } +} + +void CSMWorld::Pathgrid::load (ESM::ESMReader &esm) +{ + ESM::Pathgrid::load (esm); + + if (mCell.empty()) + { + std::ostringstream stream; + + stream << "#" << mData.mX << " " << mData.mY; + + mId = stream.str(); + } + else + mId = mCell; +} diff --git a/apps/opencs/model/world/pathgrid.hpp b/apps/opencs/model/world/pathgrid.hpp new file mode 100644 index 000000000..3c9fcff50 --- /dev/null +++ b/apps/opencs/model/world/pathgrid.hpp @@ -0,0 +1,28 @@ +#ifndef CSM_WOLRD_PATHGRID_H +#define CSM_WOLRD_PATHGRID_H + +#include +#include + +#include + +#include "idcollection.hpp" +#include "cell.hpp" + +namespace CSMWorld +{ + /// \brief Wrapper for Pathgrid record + /// + /// \attention The mData.mX and mData.mY fields of the ESM::Pathgrid struct are not used. + /// Exterior cell coordinates are encoded in the pathgrid ID. + struct Pathgrid : public ESM::Pathgrid + { + std::string mId; + + void load (ESM::ESMReader &esm, const IdCollection& cells); + + void load (ESM::ESMReader &esm); + }; +} + +#endif \ No newline at end of file diff --git a/apps/opencs/model/world/ref.cpp b/apps/opencs/model/world/ref.cpp index cf9e496ee..f3c1b0b73 100644 --- a/apps/opencs/model/world/ref.cpp +++ b/apps/opencs/model/world/ref.cpp @@ -1,12 +1,8 @@ #include "ref.hpp" -#include "cell.hpp" - -void CSMWorld::CellRef::load (ESM::ESMReader &esm, Cell& cell, const std::string& id) +CSMWorld::CellRef::CellRef() { - mId = id; - mCell = cell.mId; - - cell.addRef (mId); + mRefNum.mIndex = 0; + mRefNum.mContentFile = 0; } \ No newline at end of file diff --git a/apps/opencs/model/world/ref.hpp b/apps/opencs/model/world/ref.hpp index fcf016ee2..eb62434cf 100644 --- a/apps/opencs/model/world/ref.hpp +++ b/apps/opencs/model/world/ref.hpp @@ -3,11 +3,6 @@ #include -namespace ESM -{ - class ESMReader; -} - namespace CSMWorld { class Cell; @@ -18,8 +13,7 @@ namespace CSMWorld std::string mId; std::string mCell; - void load (ESM::ESMReader &esm, Cell& cell, const std::string& id); - ///< Load cell ref and register it with \a cell. + CellRef(); }; } diff --git a/apps/opencs/model/world/refcollection.cpp b/apps/opencs/model/world/refcollection.cpp index 4fdd97909..c516e2c3e 100644 --- a/apps/opencs/model/world/refcollection.cpp +++ b/apps/opencs/model/world/refcollection.cpp @@ -3,12 +3,15 @@ #include +#include + #include "ref.hpp" #include "cell.hpp" #include "universalid.hpp" #include "record.hpp" -void CSMWorld::RefCollection::load (ESM::ESMReader& reader, int cellIndex, bool base) +void CSMWorld::RefCollection::load (ESM::ESMReader& reader, int cellIndex, bool base, + std::map& cache, CSMDoc::Stage::Messages& messages) { Record cell = mCells.getRecord (cellIndex); @@ -17,19 +20,73 @@ void CSMWorld::RefCollection::load (ESM::ESMReader& reader, int cellIndex, bool CellRef ref; bool deleted = false; - while (cell2.getNextRef (reader, ref, deleted)) + + while (ESM::Cell::getNextRef (reader, ref, deleted)) { - /// \todo handle deleted and moved references - ref.load (reader, cell2, getNewId()); + ref.mCell = cell2.mId; - Record record2; - record2.mState = base ? RecordBase::State_BaseOnly : RecordBase::State_ModifiedOnly; - (base ? record2.mBase : record2.mModified) = ref; + /// \todo handle moved references - appendRecord (record2); - } + std::map::iterator iter = cache.find (ref.mRefNum); + + if (deleted) + { + if (iter==cache.end()) + { + CSMWorld::UniversalId id (CSMWorld::UniversalId::Type_Cell, + mCells.getId (cellIndex)); + + messages.push_back (std::make_pair (id, + "Attempt to delete a non-existing reference")); + + continue; + } + + int index = getIndex (iter->second); + + Record record = getRecord (index); + + if (record.mState==RecordBase::State_BaseOnly) + { + removeRows (index, 1); + cache.erase (iter); + } + else + { + record.mState = RecordBase::State_Deleted; + setRecord (index, record); + } - mCells.setRecord (cellIndex, cell); + continue; + } + + if (iter==cache.end()) + { + // new reference + ref.mId = getNewId(); + + Record record; + record.mState = base ? RecordBase::State_BaseOnly : RecordBase::State_ModifiedOnly; + (base ? record.mBase : record.mModified) = ref; + + appendRecord (record); + + cache.insert (std::make_pair (ref.mRefNum, ref.mId)); + } + else + { + // old reference -> merge + ref.mId = iter->second; + + int index = getIndex (ref.mId); + + Record record = getRecord (index); + record.mState = base ? RecordBase::State_BaseOnly : RecordBase::State_Modified; + (base ? record.mBase : record.mModified) = ref; + + setRecord (index, record); + } + } } std::string CSMWorld::RefCollection::getNewId() diff --git a/apps/opencs/model/world/refcollection.hpp b/apps/opencs/model/world/refcollection.hpp index 173efba05..63d369ed9 100644 --- a/apps/opencs/model/world/refcollection.hpp +++ b/apps/opencs/model/world/refcollection.hpp @@ -1,6 +1,10 @@ #ifndef CSM_WOLRD_REFCOLLECTION_H #define CSM_WOLRD_REFCOLLECTION_H +#include + +#include "../doc/stage.hpp" + #include "collection.hpp" #include "ref.hpp" #include "record.hpp" @@ -22,7 +26,9 @@ namespace CSMWorld : mCells (cells), mNextId (0) {} - void load (ESM::ESMReader& reader, int cellIndex, bool base); + void load (ESM::ESMReader& reader, int cellIndex, bool base, + std::map& cache, + CSMDoc::Stage::Messages& messages); ///< Load a sequence of references. std::string getNewId(); diff --git a/apps/opencs/model/world/refidadapterimp.cpp b/apps/opencs/model/world/refidadapterimp.cpp index f00e9fc77..d9a691abd 100644 --- a/apps/opencs/model/world/refidadapterimp.cpp +++ b/apps/opencs/model/world/refidadapterimp.cpp @@ -248,6 +248,15 @@ QVariant CSMWorld::CreatureRefIdAdapter::getData (const RefIdColumn *column, con if (column==mColumns.mOriginal) return QString::fromUtf8 (record.get().mOriginal.c_str()); + if (column==mColumns.mCombat) + return static_cast (record.get().mData.mCombat); + + if (column==mColumns.mMagic) + return static_cast (record.get().mData.mMagic); + + if (column==mColumns.mStealth) + return static_cast (record.get().mData.mStealth); + std::map::const_iterator iter = mColumns.mFlags.find (column); @@ -271,6 +280,12 @@ void CSMWorld::CreatureRefIdAdapter::setData (const RefIdColumn *column, RefIdDa record.get().mScale = value.toFloat(); else if (column==mColumns.mOriginal) record.get().mOriginal = value.toString().toUtf8().constData(); + else if (column==mColumns.mCombat) + record.get().mData.mCombat = value.toInt(); + else if (column==mColumns.mMagic) + record.get().mData.mMagic = value.toInt(); + else if (column==mColumns.mStealth) + record.get().mData.mStealth = value.toInt(); else { std::map::const_iterator iter = diff --git a/apps/opencs/model/world/refidadapterimp.hpp b/apps/opencs/model/world/refidadapterimp.hpp index bd509a86b..034905781 100644 --- a/apps/opencs/model/world/refidadapterimp.hpp +++ b/apps/opencs/model/world/refidadapterimp.hpp @@ -34,7 +34,7 @@ namespace CSMWorld BaseRefIdAdapter (UniversalId::Type type, const BaseColumns& base); virtual std::string getId (const RecordBase& record) const; - + virtual void setId (RecordBase& record, const std::string& id); virtual QVariant getData (const RefIdColumn *column, const RefIdData& data, int index) @@ -57,7 +57,7 @@ namespace CSMWorld { (dynamic_cast&> (record).get().mId) = id; } - + template std::string BaseRefIdAdapter::getId (const RecordBase& record) const { @@ -631,6 +631,9 @@ namespace CSMWorld const RefIdColumn *mSoul; const RefIdColumn *mScale; const RefIdColumn *mOriginal; + const RefIdColumn *mCombat; + const RefIdColumn *mMagic; + const RefIdColumn *mStealth; CreatureColumns (const ActorColumns& actorColumns); }; diff --git a/apps/opencs/model/world/refidcollection.cpp b/apps/opencs/model/world/refidcollection.cpp index f515e34d8..779d5a40c 100644 --- a/apps/opencs/model/world/refidcollection.cpp +++ b/apps/opencs/model/world/refidcollection.cpp @@ -44,7 +44,7 @@ CSMWorld::RefIdCollection::RefIdCollection() ColumnBase::Flag_Table | ColumnBase::Flag_Dialogue, false, false)); baseColumns.mId = &mColumns.back(); mColumns.push_back (RefIdColumn (Columns::ColumnId_Modification, ColumnBase::Display_RecordState, - ColumnBase::Flag_Table | ColumnBase::Flag_Dialogue, false, false)); + ColumnBase::Flag_Table | ColumnBase::Flag_Dialogue, true, false)); baseColumns.mModified = &mColumns.back(); mColumns.push_back (RefIdColumn (Columns::ColumnId_RecordType, ColumnBase::Display_RefRecordType, ColumnBase::Flag_Table | ColumnBase::Flag_Dialogue, false, false)); @@ -52,7 +52,7 @@ CSMWorld::RefIdCollection::RefIdCollection() ModelColumns modelColumns (baseColumns); - mColumns.push_back (RefIdColumn (Columns::ColumnId_Model, ColumnBase::Display_String)); + mColumns.push_back (RefIdColumn (Columns::ColumnId_Model, ColumnBase::Display_Mesh)); modelColumns.mModel = &mColumns.back(); NameColumns nameColumns (modelColumns); @@ -64,7 +64,7 @@ CSMWorld::RefIdCollection::RefIdCollection() InventoryColumns inventoryColumns (nameColumns); - mColumns.push_back (RefIdColumn (Columns::ColumnId_Icon, ColumnBase::Display_String)); + mColumns.push_back (RefIdColumn (Columns::ColumnId_Icon, ColumnBase::Display_Icon)); inventoryColumns.mIcon = &mColumns.back(); mColumns.push_back (RefIdColumn (Columns::ColumnId_Weight, ColumnBase::Display_Float)); inventoryColumns.mWeight = &mColumns.back(); @@ -175,6 +175,15 @@ CSMWorld::RefIdCollection::RefIdCollection() creatureColumns.mScale = &mColumns.back(); mColumns.push_back (RefIdColumn (Columns::ColumnId_OriginalCreature, ColumnBase::Display_String)); creatureColumns.mOriginal = &mColumns.back(); + mColumns.push_back ( + RefIdColumn (Columns::ColumnId_CombatState, ColumnBase::Display_Integer)); + creatureColumns.mCombat = &mColumns.back(); + mColumns.push_back ( + RefIdColumn (Columns::ColumnId_MagicState, ColumnBase::Display_Integer)); + creatureColumns.mMagic = &mColumns.back(); + mColumns.push_back ( + RefIdColumn (Columns::ColumnId_StealthState, ColumnBase::Display_Integer)); + creatureColumns.mStealth = &mColumns.back(); static const struct { @@ -304,10 +313,17 @@ CSMWorld::RefIdCollection::RefIdCollection() mColumns.push_back (RefIdColumn (Columns::ColumnId_WeaponReach, ColumnBase::Display_Float)); weaponColumns.mReach = &mColumns.back(); - for (int i=0; i<6; ++i) + for (int i=0; i<3; ++i) { - mColumns.push_back (RefIdColumn (Columns::ColumnId_MinChop + i, ColumnBase::Display_Integer)); - weaponColumns.mChop[i] = &mColumns.back(); + const RefIdColumn **column = + i==0 ? weaponColumns.mChop : (i==1 ? weaponColumns.mSlash : weaponColumns.mThrust); + + for (int j=0; j<2; ++j) + { + mColumns.push_back ( + RefIdColumn (Columns::ColumnId_MinChop+i*2+j, ColumnBase::Display_Integer)); + column[j] = &mColumns.back(); + } } static const struct diff --git a/apps/opencs/model/world/resources.cpp b/apps/opencs/model/world/resources.cpp new file mode 100644 index 000000000..8e255bc96 --- /dev/null +++ b/apps/opencs/model/world/resources.cpp @@ -0,0 +1,103 @@ + +#include "resources.hpp" + +#include +#include + +#include + +#include + +CSMWorld::Resources::Resources (const std::string& baseDirectory, UniversalId::Type type, + const char * const *extensions) +: mBaseDirectory (baseDirectory), mType (type) +{ + int baseSize = mBaseDirectory.size(); + + Ogre::StringVector resourcesGroups = + Ogre::ResourceGroupManager::getSingleton().getResourceGroups(); + + for (Ogre::StringVector::iterator iter (resourcesGroups.begin()); + iter!=resourcesGroups.end(); ++iter) + { + if (*iter=="General" || *iter=="Internal" || *iter=="Autodetect") + continue; + + Ogre::StringVectorPtr resources = + Ogre::ResourceGroupManager::getSingleton().listResourceNames (*iter); + + for (Ogre::StringVector::const_iterator iter (resources->begin()); + iter!=resources->end(); ++iter) + { + if (static_cast (iter->size())substr (0, baseSize)!=mBaseDirectory || + ((*iter)[baseSize]!='/' && (*iter)[baseSize]!='\\')) + continue; + + if (extensions) + { + std::string::size_type index = iter->find_last_of ('.'); + + if (index==std::string::npos) + continue; + + std::string extension = iter->substr (index+1); + + int i = 0; + + for (; extensions[i]; ++i) + if (extensions[i]==extension) + break; + + if (!extensions[i]) + continue; + } + + std::string file = iter->substr (baseSize+1); + mFiles.push_back (file); + mIndex.insert (std::make_pair (file, static_cast (mFiles.size())-1)); + } + } +} + +int CSMWorld::Resources::getSize() const +{ + return mFiles.size(); +} + +std::string CSMWorld::Resources::getId (int index) const +{ + return mFiles.at (index); +} + +int CSMWorld::Resources::getIndex (const std::string& id) const +{ + int index = searchId (id); + + if (index==-1) + { + std::ostringstream stream; + stream << "Invalid resource: " << mBaseDirectory << '/' << id; + + throw std::runtime_error (stream.str().c_str()); + } + + return index; +} + +int CSMWorld::Resources::searchId (const std::string& id) const +{ + std::string id2 = Misc::StringUtils::lowerCase (id); + + std::map::const_iterator iter = mIndex.find (id2); + + if (iter==mIndex.end()) + return -1; + + return iter->second; +} + +CSMWorld::UniversalId::Type CSMWorld::Resources::getType() const +{ + return mType; +} \ No newline at end of file diff --git a/apps/opencs/model/world/resources.hpp b/apps/opencs/model/world/resources.hpp new file mode 100644 index 000000000..9c1c76b6f --- /dev/null +++ b/apps/opencs/model/world/resources.hpp @@ -0,0 +1,37 @@ +#ifndef CSM_WOLRD_RESOURCES_H +#define CSM_WOLRD_RESOURCES_H + +#include +#include +#include + +#include "universalid.hpp" + +namespace CSMWorld +{ + class Resources + { + std::map mIndex; + std::vector mFiles; + std::string mBaseDirectory; + UniversalId::Type mType; + + public: + + /// \param type Type of resources in this table. + Resources (const std::string& baseDirectory, UniversalId::Type type, + const char * const *extensions = 0); + + int getSize() const; + + std::string getId (int index) const; + + int getIndex (const std::string& id) const; + + int searchId (const std::string& id) const; + + UniversalId::Type getType() const; + }; +} + +#endif diff --git a/apps/opencs/model/world/resourcesmanager.cpp b/apps/opencs/model/world/resourcesmanager.cpp new file mode 100644 index 000000000..50014f4b5 --- /dev/null +++ b/apps/opencs/model/world/resourcesmanager.cpp @@ -0,0 +1,33 @@ + +#include "resourcesmanager.hpp" + +#include + +void CSMWorld::ResourcesManager::addResources (const Resources& resources) +{ + mResources.insert (std::make_pair (resources.getType(), resources)); + mResources.insert (std::make_pair (UniversalId::getParentType (resources.getType()), + resources)); +} + +void CSMWorld::ResourcesManager::listResources() +{ + static const char * const sMeshTypes[] = { "nif", 0 }; + + addResources (Resources ("meshes", UniversalId::Type_Mesh, sMeshTypes)); + addResources (Resources ("icons", UniversalId::Type_Icon)); + addResources (Resources ("music", UniversalId::Type_Music)); + addResources (Resources ("sound", UniversalId::Type_SoundRes)); + addResources (Resources ("textures", UniversalId::Type_Texture)); + addResources (Resources ("videos", UniversalId::Type_Video)); +} + +const CSMWorld::Resources& CSMWorld::ResourcesManager::get (UniversalId::Type type) const +{ + std::map::const_iterator iter = mResources.find (type); + + if (iter==mResources.end()) + throw std::logic_error ("Unknown resource type"); + + return iter->second; +} \ No newline at end of file diff --git a/apps/opencs/model/world/resourcesmanager.hpp b/apps/opencs/model/world/resourcesmanager.hpp new file mode 100644 index 000000000..77f210c47 --- /dev/null +++ b/apps/opencs/model/world/resourcesmanager.hpp @@ -0,0 +1,28 @@ +#ifndef CSM_WOLRD_RESOURCESMANAGER_H +#define CSM_WOLRD_RESOURCESMANAGER_H + +#include + +#include "universalid.hpp" +#include "resources.hpp" + +namespace CSMWorld +{ + class ResourcesManager + { + std::map mResources; + + private: + + void addResources (const Resources& resources); + + public: + + /// Ask OGRE for a list of available resources. + void listResources(); + + const Resources& get (UniversalId::Type type) const; + }; +} + +#endif \ No newline at end of file diff --git a/apps/opencs/model/world/resourcetable.cpp b/apps/opencs/model/world/resourcetable.cpp new file mode 100644 index 000000000..a7180434a --- /dev/null +++ b/apps/opencs/model/world/resourcetable.cpp @@ -0,0 +1,146 @@ + +#include "resourcetable.hpp" + +#include + +#include "resources.hpp" +#include "columnbase.hpp" +#include "universalid.hpp" + +CSMWorld::ResourceTable::ResourceTable (const Resources *resources, unsigned int features) +: IdTableBase (features | Feature_Constant), mResources (resources) +{} + +CSMWorld::ResourceTable::~ResourceTable() {} + +int CSMWorld::ResourceTable::rowCount (const QModelIndex & parent) const +{ + if (parent.isValid()) + return 0; + + return mResources->getSize(); +} + +int CSMWorld::ResourceTable::columnCount (const QModelIndex & parent) const +{ + if (parent.isValid()) + return 0; + + return 2; // ID, type +} + +QVariant CSMWorld::ResourceTable::data (const QModelIndex & index, int role) const +{ + if (role!=Qt::DisplayRole) + return QVariant(); + + if (index.column()==0) + return QString::fromUtf8 (mResources->getId (index.row()).c_str()); + + if (index.column()==1) + return static_cast (mResources->getType()); + + throw std::logic_error ("Invalid column in resource table"); +} + +QVariant CSMWorld::ResourceTable::headerData (int section, Qt::Orientation orientation, + int role ) const +{ + if (orientation==Qt::Vertical) + return QVariant(); + + if (role==ColumnBase::Role_Flags) + return section==0 ? ColumnBase::Flag_Table : 0; + + switch (section) + { + case 0: + + if (role==Qt::DisplayRole) + return Columns::getName (Columns::ColumnId_Id).c_str(); + + if (role==ColumnBase::Role_Display) + return ColumnBase::Display_String; + + break; + + case 1: + + if (role==Qt::DisplayRole) + return Columns::getName (Columns::ColumnId_RecordType).c_str(); + + if (role==ColumnBase::Role_Display) + return ColumnBase::Display_Integer; + + break; + } + + return QVariant(); +} + +bool CSMWorld::ResourceTable::setData ( const QModelIndex &index, const QVariant &value, + int role) +{ + return false; +} + +Qt::ItemFlags CSMWorld::ResourceTable::flags (const QModelIndex & index) const +{ + return Qt::ItemIsSelectable | Qt::ItemIsEnabled; +} + +QModelIndex CSMWorld::ResourceTable::index (int row, int column, const QModelIndex& parent) + const +{ + if (parent.isValid()) + return QModelIndex(); + + if (row<0 || row>=mResources->getSize()) + return QModelIndex(); + + if (column<0 || column>1) + return QModelIndex(); + + return createIndex (row, column); +} + +QModelIndex CSMWorld::ResourceTable::parent (const QModelIndex& index) const +{ + return QModelIndex(); +} + +QModelIndex CSMWorld::ResourceTable::getModelIndex (const std::string& id, int column) const +{ + return index (mResources->getIndex (id), column); +} + +int CSMWorld::ResourceTable::searchColumnIndex (Columns::ColumnId id) const +{ + if (id==Columns::ColumnId_Id) + return 0; + + if (id==Columns::ColumnId_RecordType) + return 1; + + return -1; +} + +int CSMWorld::ResourceTable::findColumnIndex (Columns::ColumnId id) const +{ + int index = searchColumnIndex (id); + + if (index==-1) + throw std::logic_error ("invalid column index"); + + return index; +} + +std::pair CSMWorld::ResourceTable::view (int row) const +{ + return std::make_pair (UniversalId::Type_None, ""); +} + +bool CSMWorld::ResourceTable::isDeleted (const std::string& id) const +{ + return false; +} \ No newline at end of file diff --git a/apps/opencs/model/world/resourcetable.hpp b/apps/opencs/model/world/resourcetable.hpp new file mode 100644 index 000000000..f5011ab2b --- /dev/null +++ b/apps/opencs/model/world/resourcetable.hpp @@ -0,0 +1,57 @@ +#ifndef CSM_WOLRD_RESOURCETABLE_H +#define CSM_WOLRD_RESOURCETABLE_H + +#include "idtablebase.hpp" + +namespace CSMWorld +{ + class Resources; + + class ResourceTable : public IdTableBase + { + const Resources *mResources; + + public: + + /// \note The feature Feature_Constant will be added implicitly. + ResourceTable (const Resources *resources, unsigned int features = 0); + + virtual ~ResourceTable(); + + virtual int rowCount (const QModelIndex & parent = QModelIndex()) const; + + virtual int columnCount (const QModelIndex & parent = QModelIndex()) const; + + virtual QVariant data (const QModelIndex & index, int role = Qt::DisplayRole) const; + + virtual QVariant headerData (int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const; + + virtual bool setData ( const QModelIndex &index, const QVariant &value, int role = Qt::EditRole); + + virtual Qt::ItemFlags flags (const QModelIndex & index) const; + + virtual QModelIndex index (int row, int column, const QModelIndex& parent = QModelIndex()) + const; + + virtual QModelIndex parent (const QModelIndex& index) const; + + virtual QModelIndex getModelIndex (const std::string& id, int column) const; + + /// Return index of column with the given \a id. If no such column exists, -1 is + /// returned. + virtual int searchColumnIndex (Columns::ColumnId id) const; + + /// Return index of column with the given \a id. If no such column exists, an + /// exception is thrown. + virtual int findColumnIndex (Columns::ColumnId id) const; + + /// Return the UniversalId and the hint for viewing \a row. If viewing is not + /// supported by this table, return (UniversalId::Type_None, ""). + virtual std::pair view (int row) const; + + /// Is \a id flagged as deleted? + virtual bool isDeleted (const std::string& id) const; + }; +} + +#endif diff --git a/apps/opencs/model/world/scope.cpp b/apps/opencs/model/world/scope.cpp new file mode 100644 index 000000000..e3ebf5ebd --- /dev/null +++ b/apps/opencs/model/world/scope.cpp @@ -0,0 +1,25 @@ + +#include "scope.hpp" + +#include + +#include + +CSMWorld::Scope CSMWorld::getScopeFromId (const std::string& id) +{ + // get root namespace + std::string namespace_; + + std::string::size_type i = id.find ("::"); + + if (i!=std::string::npos) + namespace_ = Misc::StringUtils::lowerCase (id.substr (0, i)); + + if (namespace_=="project") + return Scope_Project; + + if (namespace_=="session") + return Scope_Session; + + return Scope_Content; +} \ No newline at end of file diff --git a/apps/opencs/model/world/scope.hpp b/apps/opencs/model/world/scope.hpp new file mode 100644 index 000000000..3983d91f5 --- /dev/null +++ b/apps/opencs/model/world/scope.hpp @@ -0,0 +1,23 @@ +#ifndef CSM_WOLRD_SCOPE_H +#define CSM_WOLRD_SCOPE_H + +#include + +namespace CSMWorld +{ + enum Scope + { + // record stored in content file + Scope_Content = 1, + + // record stored in project file + Scope_Project = 2, + + // record that exists only for the duration of one editing session + Scope_Session = 4 + }; + + Scope getScopeFromId (const std::string& id); +} + +#endif diff --git a/apps/opencs/model/world/scriptcontext.cpp b/apps/opencs/model/world/scriptcontext.cpp index 9da49defe..0d2b9984e 100644 --- a/apps/opencs/model/world/scriptcontext.cpp +++ b/apps/opencs/model/world/scriptcontext.cpp @@ -47,7 +47,7 @@ std::pair CSMWorld::ScriptContext::getMemberType (const std::string& int index = mData.getScripts().searchId (id2); bool reference = false; - if (index!=-1) + if (index==-1) { // ID is not a script ID. Search for a matching referenceable instead. index = mData.getReferenceables().searchId (id2); @@ -55,19 +55,16 @@ std::pair CSMWorld::ScriptContext::getMemberType (const std::string& if (index!=-1) { // Referenceable found. - int columnIndex = mData.getReferenceables().searchColumnIndex (Columns::ColumnId_Script); + int columnIndex = mData.getReferenceables().findColumnIndex (Columns::ColumnId_Script); + + id2 = Misc::StringUtils::lowerCase (mData.getReferenceables(). + getData (index, columnIndex).toString().toUtf8().constData()); - if (columnIndex!=-1) + if (!id2.empty()) { - id2 = Misc::StringUtils::lowerCase (mData.getReferenceables(). - getData (index, columnIndex).toString().toUtf8().constData()); - - if (!id2.empty()) - { - // Referenceable has a script -> use it. - index = mData.getScripts().searchId (id2); - reference = true; - } + // Referenceable has a script -> use it. + index = mData.getScripts().searchId (id2); + reference = true; } } } @@ -99,7 +96,7 @@ bool CSMWorld::ScriptContext::isId (const std::string& name) const { mIds = mData.getIds(); - std::for_each (mIds.begin(), mIds.end(), &Misc::StringUtils::lowerCase); + std::for_each (mIds.begin(), mIds.end(), &Misc::StringUtils::toLower); std::sort (mIds.begin(), mIds.end()); mIdsUpdated = true; diff --git a/apps/opencs/model/world/subcellcollection.hpp b/apps/opencs/model/world/subcellcollection.hpp new file mode 100644 index 000000000..28f0de695 --- /dev/null +++ b/apps/opencs/model/world/subcellcollection.hpp @@ -0,0 +1,38 @@ +#ifndef CSM_WOLRD_SUBCOLLECTION_H +#define CSM_WOLRD_SUBCOLLECTION_H + +#include "idcollection.hpp" + +namespace CSMWorld +{ + /// \brief Single type collection of top level records that are associated with cells + template > + class SubCellCollection : public IdCollection + { + const IdCollection& mCells; + + virtual void loadRecord (ESXRecordT& record, ESM::ESMReader& reader); + + public: + + SubCellCollection (const IdCollection& cells); + + + }; + + template + void SubCellCollection::loadRecord (ESXRecordT& record, + ESM::ESMReader& reader) + { + record.load (reader, mCells); + } + + template + SubCellCollection::SubCellCollection ( + const IdCollection& cells) + : mCells (cells) + {} + +} + +#endif diff --git a/apps/opencs/model/world/tablemimedata.cpp b/apps/opencs/model/world/tablemimedata.cpp index 6d65d0ff8..d40e0c217 100644 --- a/apps/opencs/model/world/tablemimedata.cpp +++ b/apps/opencs/model/world/tablemimedata.cpp @@ -37,7 +37,7 @@ std::string CSMWorld::TableMimeData::getIcon() const if (mUniversalId.empty()) { qDebug()<<"TableMimeData object does not hold any records!"; //because throwing in the event loop tends to be problematic - throw("TableMimeData object does not hold any records!"); + throw std::runtime_error ("TableMimeData object does not hold any records!"); } std::string tmpIcon; @@ -91,7 +91,7 @@ return ( type == CSMWorld::ColumnBase::Display_Activator || type == CSMWorld::ColumnBase::Display_Static || type == CSMWorld::ColumnBase::Display_Weapon); } -bool CSMWorld::TableMimeData::isReferencable(CSMWorld::UniversalId::Type type) const +bool CSMWorld::TableMimeData::isReferencable(CSMWorld::UniversalId::Type type) { return ( type == CSMWorld::UniversalId::Type_Activator || type == CSMWorld::UniversalId::Type_Potion @@ -179,7 +179,7 @@ CSMWorld::UniversalId CSMWorld::TableMimeData::returnMatching (CSMWorld::Univers } } - throw ("TableMimeData object does not hold object of the seeked type"); + throw std::runtime_error ("TableMimeData object does not hold object of the seeked type"); } CSMWorld::UniversalId CSMWorld::TableMimeData::returnMatching (CSMWorld::ColumnBase::Display type) const @@ -201,7 +201,7 @@ CSMWorld::UniversalId CSMWorld::TableMimeData::returnMatching (CSMWorld::ColumnB } } - throw ("TableMimeData object does not hold object of the seeked type"); + throw std::runtime_error ("TableMimeData object does not hold object of the seeked type"); } bool CSMWorld::TableMimeData::fromDocument (const CSMDoc::Document& document) const @@ -209,327 +209,82 @@ bool CSMWorld::TableMimeData::fromDocument (const CSMDoc::Document& document) co return &document == &mDocument; } -CSMWorld::UniversalId::Type CSMWorld::TableMimeData::convertEnums (CSMWorld::ColumnBase::Display type) +namespace { - switch (type) + struct Mapping { - case CSMWorld::ColumnBase::Display_Race: - return CSMWorld::UniversalId::Type_Race; + CSMWorld::UniversalId::Type mUniversalIdType; + CSMWorld::ColumnBase::Display mDisplayType; + }; - - case CSMWorld::ColumnBase::Display_Skill: - return CSMWorld::UniversalId::Type_Skill; - - - case CSMWorld::ColumnBase::Display_Class: - return CSMWorld::UniversalId::Type_Class; - - - case CSMWorld::ColumnBase::Display_Faction: - return CSMWorld::UniversalId::Type_Faction; - - - case CSMWorld::ColumnBase::Display_Sound: - return CSMWorld::UniversalId::Type_Sound; - - - case CSMWorld::ColumnBase::Display_Region: - return CSMWorld::UniversalId::Type_Region; - - - case CSMWorld::ColumnBase::Display_Birthsign: - return CSMWorld::UniversalId::Type_Birthsign; - - - case CSMWorld::ColumnBase::Display_Spell: - return CSMWorld::UniversalId::Type_Spell; - - - case CSMWorld::ColumnBase::Display_Cell: - return CSMWorld::UniversalId::Type_Cell; - - - case CSMWorld::ColumnBase::Display_Referenceable: - return CSMWorld::UniversalId::Type_Referenceable; - - - case CSMWorld::ColumnBase::Display_Activator: - return CSMWorld::UniversalId::Type_Activator; - - - case CSMWorld::ColumnBase::Display_Potion: - return CSMWorld::UniversalId::Type_Potion; - - - case CSMWorld::ColumnBase::Display_Apparatus: - return CSMWorld::UniversalId::Type_Apparatus; - - - case CSMWorld::ColumnBase::Display_Armor: - return CSMWorld::UniversalId::Type_Armor; - - - case CSMWorld::ColumnBase::Display_Book: - return CSMWorld::UniversalId::Type_Book; - - - case CSMWorld::ColumnBase::Display_Clothing: - return CSMWorld::UniversalId::Type_Clothing; - - - case CSMWorld::ColumnBase::Display_Container: - return CSMWorld::UniversalId::Type_Container; - - - case CSMWorld::ColumnBase::Display_Creature: - return CSMWorld::UniversalId::Type_Creature; - - - case CSMWorld::ColumnBase::Display_Door: - return CSMWorld::UniversalId::Type_Door; - - - case CSMWorld::ColumnBase::Display_Ingredient: - return CSMWorld::UniversalId::Type_Ingredient; - - - case CSMWorld::ColumnBase::Display_CreatureLevelledList: - return CSMWorld::UniversalId::Type_CreatureLevelledList; - - - case CSMWorld::ColumnBase::Display_ItemLevelledList: - return CSMWorld::UniversalId::Type_ItemLevelledList; - - - case CSMWorld::ColumnBase::Display_Light: - return CSMWorld::UniversalId::Type_Light; - - - case CSMWorld::ColumnBase::Display_Lockpick: - return CSMWorld::UniversalId::Type_Lockpick; - - - case CSMWorld::ColumnBase::Display_Miscellaneous: - return CSMWorld::UniversalId::Type_Miscellaneous; - - - case CSMWorld::ColumnBase::Display_Npc: - return CSMWorld::UniversalId::Type_Npc; - - - case CSMWorld::ColumnBase::Display_Probe: - return CSMWorld::UniversalId::Type_Probe; - - - case CSMWorld::ColumnBase::Display_Repair: - return CSMWorld::UniversalId::Type_Repair; - - - case CSMWorld::ColumnBase::Display_Static: - return CSMWorld::UniversalId::Type_Static; - - - case CSMWorld::ColumnBase::Display_Weapon: - return CSMWorld::UniversalId::Type_Weapon; - - - case CSMWorld::ColumnBase::Display_Reference: - return CSMWorld::UniversalId::Type_Reference; - - - case CSMWorld::ColumnBase::Display_Filter: - return CSMWorld::UniversalId::Type_Filter; - - - case CSMWorld::ColumnBase::Display_Topic: - return CSMWorld::UniversalId::Type_Topic; - - - case CSMWorld::ColumnBase::Display_Journal: - return CSMWorld::UniversalId::Type_Journal; - - - case CSMWorld::ColumnBase::Display_TopicInfo: - return CSMWorld::UniversalId::Type_TopicInfo; - - - case CSMWorld::ColumnBase::Display_JournalInfo: - return CSMWorld::UniversalId::Type_JournalInfo; - - - case CSMWorld::ColumnBase::Display_Scene: - return CSMWorld::UniversalId::Type_Scene; - - - case CSMWorld::ColumnBase::Display_Script: - return CSMWorld::UniversalId::Type_Script; - - - default: - return CSMWorld::UniversalId::Type_None; - - } + const Mapping mapping[] = + { + { CSMWorld::UniversalId::Type_Race, CSMWorld::ColumnBase::Display_Race }, + { CSMWorld::UniversalId::Type_Skill, CSMWorld::ColumnBase::Display_Skill }, + { CSMWorld::UniversalId::Type_Class, CSMWorld::ColumnBase::Display_Class }, + { CSMWorld::UniversalId::Type_Faction, CSMWorld::ColumnBase::Display_Faction }, + { CSMWorld::UniversalId::Type_Sound, CSMWorld::ColumnBase::Display_Sound }, + { CSMWorld::UniversalId::Type_Region, CSMWorld::ColumnBase::Display_Region }, + { CSMWorld::UniversalId::Type_Birthsign, CSMWorld::ColumnBase::Display_Birthsign }, + { CSMWorld::UniversalId::Type_Spell, CSMWorld::ColumnBase::Display_Spell }, + { CSMWorld::UniversalId::Type_Cell, CSMWorld::ColumnBase::Display_Cell }, + { CSMWorld::UniversalId::Type_Referenceable, CSMWorld::ColumnBase::Display_Referenceable }, + { CSMWorld::UniversalId::Type_Activator, CSMWorld::ColumnBase::Display_Activator }, + { CSMWorld::UniversalId::Type_Potion, CSMWorld::ColumnBase::Display_Potion }, + { CSMWorld::UniversalId::Type_Apparatus, CSMWorld::ColumnBase::Display_Apparatus }, + { CSMWorld::UniversalId::Type_Armor, CSMWorld::ColumnBase::Display_Armor }, + { CSMWorld::UniversalId::Type_Book, CSMWorld::ColumnBase::Display_Book }, + { CSMWorld::UniversalId::Type_Clothing, CSMWorld::ColumnBase::Display_Clothing }, + { CSMWorld::UniversalId::Type_Container, CSMWorld::ColumnBase::Display_Container }, + { CSMWorld::UniversalId::Type_Creature, CSMWorld::ColumnBase::Display_Creature }, + { CSMWorld::UniversalId::Type_Door, CSMWorld::ColumnBase::Display_Door }, + { CSMWorld::UniversalId::Type_Ingredient, CSMWorld::ColumnBase::Display_Ingredient }, + { CSMWorld::UniversalId::Type_CreatureLevelledList, CSMWorld::ColumnBase::Display_CreatureLevelledList }, + { CSMWorld::UniversalId::Type_ItemLevelledList, CSMWorld::ColumnBase::Display_ItemLevelledList }, + { CSMWorld::UniversalId::Type_Light, CSMWorld::ColumnBase::Display_Light }, + { CSMWorld::UniversalId::Type_Lockpick, CSMWorld::ColumnBase::Display_Lockpick }, + { CSMWorld::UniversalId::Type_Miscellaneous, CSMWorld::ColumnBase::Display_Miscellaneous }, + { CSMWorld::UniversalId::Type_Npc, CSMWorld::ColumnBase::Display_Npc }, + { CSMWorld::UniversalId::Type_Probe, CSMWorld::ColumnBase::Display_Probe }, + { CSMWorld::UniversalId::Type_Repair, CSMWorld::ColumnBase::Display_Repair }, + { CSMWorld::UniversalId::Type_Static, CSMWorld::ColumnBase::Display_Static }, + { CSMWorld::UniversalId::Type_Weapon, CSMWorld::ColumnBase::Display_Weapon }, + { CSMWorld::UniversalId::Type_Reference, CSMWorld::ColumnBase::Display_Reference }, + { CSMWorld::UniversalId::Type_Filter, CSMWorld::ColumnBase::Display_Filter }, + { CSMWorld::UniversalId::Type_Topic, CSMWorld::ColumnBase::Display_Topic }, + { CSMWorld::UniversalId::Type_Journal, CSMWorld::ColumnBase::Display_Journal }, + { CSMWorld::UniversalId::Type_TopicInfo, CSMWorld::ColumnBase::Display_TopicInfo }, + { CSMWorld::UniversalId::Type_JournalInfo, CSMWorld::ColumnBase::Display_JournalInfo }, + { CSMWorld::UniversalId::Type_Scene, CSMWorld::ColumnBase::Display_Scene }, + { CSMWorld::UniversalId::Type_Script, CSMWorld::ColumnBase::Display_Script }, + { CSMWorld::UniversalId::Type_Mesh, CSMWorld::ColumnBase::Display_Mesh }, + { CSMWorld::UniversalId::Type_Icon, CSMWorld::ColumnBase::Display_Icon }, + { CSMWorld::UniversalId::Type_Music, CSMWorld::ColumnBase::Display_Music }, + { CSMWorld::UniversalId::Type_SoundRes, CSMWorld::ColumnBase::Display_SoundRes }, + { CSMWorld::UniversalId::Type_Texture, CSMWorld::ColumnBase::Display_Texture }, + { CSMWorld::UniversalId::Type_Video, CSMWorld::ColumnBase::Display_Video }, + { CSMWorld::UniversalId::Type_Global, CSMWorld::ColumnBase::Display_GlobalVariable }, + + { CSMWorld::UniversalId::Type_None, CSMWorld::ColumnBase::Display_None } // end marker + }; } -CSMWorld::ColumnBase::Display CSMWorld::TableMimeData::convertEnums (CSMWorld::UniversalId::Type type) +CSMWorld::UniversalId::Type CSMWorld::TableMimeData::convertEnums (ColumnBase::Display type) { - switch (type) - { - case CSMWorld::UniversalId::Type_Race: - return CSMWorld::ColumnBase::Display_Race; - - - case CSMWorld::UniversalId::Type_Skill: - return CSMWorld::ColumnBase::Display_Skill; - - - case CSMWorld::UniversalId::Type_Class: - return CSMWorld::ColumnBase::Display_Class; - - - case CSMWorld::UniversalId::Type_Faction: - return CSMWorld::ColumnBase::Display_Faction; - - - case CSMWorld::UniversalId::Type_Sound: - return CSMWorld::ColumnBase::Display_Sound; - - - case CSMWorld::UniversalId::Type_Region: - return CSMWorld::ColumnBase::Display_Region; - - - case CSMWorld::UniversalId::Type_Birthsign: - return CSMWorld::ColumnBase::Display_Birthsign; - - - case CSMWorld::UniversalId::Type_Spell: - return CSMWorld::ColumnBase::Display_Spell; - - - case CSMWorld::UniversalId::Type_Cell: - return CSMWorld::ColumnBase::Display_Cell; - - - case CSMWorld::UniversalId::Type_Referenceable: - return CSMWorld::ColumnBase::Display_Referenceable; - - - case CSMWorld::UniversalId::Type_Activator: - return CSMWorld::ColumnBase::Display_Activator; - - - case CSMWorld::UniversalId::Type_Potion: - return CSMWorld::ColumnBase::Display_Potion; - - - case CSMWorld::UniversalId::Type_Apparatus: - return CSMWorld::ColumnBase::Display_Apparatus; - - - case CSMWorld::UniversalId::Type_Armor: - return CSMWorld::ColumnBase::Display_Armor; - - - case CSMWorld::UniversalId::Type_Book: - return CSMWorld::ColumnBase::Display_Book; - - - case CSMWorld::UniversalId::Type_Clothing: - return CSMWorld::ColumnBase::Display_Clothing; - - - case CSMWorld::UniversalId::Type_Container: - return CSMWorld::ColumnBase::Display_Container; - - - case CSMWorld::UniversalId::Type_Creature: - return CSMWorld::ColumnBase::Display_Creature; - - - case CSMWorld::UniversalId::Type_Door: - return CSMWorld::ColumnBase::Display_Door; - - - case CSMWorld::UniversalId::Type_Ingredient: - return CSMWorld::ColumnBase::Display_Ingredient; + for (int i=0; mapping[i].mUniversalIdType!=CSMWorld::UniversalId::Type_None; ++i) + if (mapping[i].mDisplayType==type) + return mapping[i].mUniversalIdType; + return CSMWorld::UniversalId::Type_None; +} - case CSMWorld::UniversalId::Type_CreatureLevelledList: - return CSMWorld::ColumnBase::Display_CreatureLevelledList; - - - case CSMWorld::UniversalId::Type_ItemLevelledList: - return CSMWorld::ColumnBase::Display_ItemLevelledList; - - - case CSMWorld::UniversalId::Type_Light: - return CSMWorld::ColumnBase::Display_Light; - - - case CSMWorld::UniversalId::Type_Lockpick: - return CSMWorld::ColumnBase::Display_Lockpick; - - - case CSMWorld::UniversalId::Type_Miscellaneous: - return CSMWorld::ColumnBase::Display_Miscellaneous; - - - case CSMWorld::UniversalId::Type_Npc: - return CSMWorld::ColumnBase::Display_Npc; - - - case CSMWorld::UniversalId::Type_Probe: - return CSMWorld::ColumnBase::Display_Probe; - - - case CSMWorld::UniversalId::Type_Repair: - return CSMWorld::ColumnBase::Display_Repair; - - - case CSMWorld::UniversalId::Type_Static: - return CSMWorld::ColumnBase::Display_Static; - - - case CSMWorld::UniversalId::Type_Weapon: - return CSMWorld::ColumnBase::Display_Weapon; - - - case CSMWorld::UniversalId::Type_Reference: - return CSMWorld::ColumnBase::Display_Reference; - - - case CSMWorld::UniversalId::Type_Filter: - return CSMWorld::ColumnBase::Display_Filter; - - - case CSMWorld::UniversalId::Type_Topic: - return CSMWorld::ColumnBase::Display_Topic; - - - case CSMWorld::UniversalId::Type_Journal: - return CSMWorld::ColumnBase::Display_Journal; - - - case CSMWorld::UniversalId::Type_TopicInfo: - return CSMWorld::ColumnBase::Display_TopicInfo; - - - case CSMWorld::UniversalId::Type_JournalInfo: - return CSMWorld::ColumnBase::Display_JournalInfo; - - - case CSMWorld::UniversalId::Type_Scene: - return CSMWorld::ColumnBase::Display_Scene; - - - case CSMWorld::UniversalId::Type_Script: - return CSMWorld::ColumnBase::Display_Script; - +CSMWorld::ColumnBase::Display CSMWorld::TableMimeData::convertEnums (UniversalId::Type type) +{ + for (int i=0; mapping[i].mUniversalIdType!=CSMWorld::UniversalId::Type_None; ++i) + if (mapping[i].mUniversalIdType==type) + return mapping[i].mDisplayType; - default: - return CSMWorld::ColumnBase::Display_None; - } + return CSMWorld::ColumnBase::Display_None; } const CSMDoc::Document* CSMWorld::TableMimeData::getDocumentPtr() const diff --git a/apps/opencs/model/world/tablemimedata.hpp b/apps/opencs/model/world/tablemimedata.hpp index 85c243944..06d252435 100644 --- a/apps/opencs/model/world/tablemimedata.hpp +++ b/apps/opencs/model/world/tablemimedata.hpp @@ -59,10 +59,10 @@ namespace CSMWorld static CSMWorld::ColumnBase::Display convertEnums(CSMWorld::UniversalId::Type type); + static bool isReferencable(CSMWorld::UniversalId::Type type); private: - bool isReferencable(CSMWorld::UniversalId::Type type) const; bool isReferencable(CSMWorld::ColumnBase::Display type) const; }; } -#endif // TABLEMIMEDATA_H \ No newline at end of file +#endif // TABLEMIMEDATA_H diff --git a/apps/opencs/model/world/universalid.cpp b/apps/opencs/model/world/universalid.cpp index 140a410c0..190ea87d8 100644 --- a/apps/opencs/model/world/universalid.cpp +++ b/apps/opencs/model/world/universalid.cpp @@ -35,6 +35,8 @@ namespace { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_TopicInfos, "Topic Infos", 0 }, { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_JournalInfos, "Journal Infos", 0 }, { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Cells, "Cells", 0 }, + { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Enchantments, "Enchantments", 0 }, + { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_BodyParts, "Body Parts", 0 }, { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Referenceables, "Referenceables", 0 }, { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_References, @@ -42,6 +44,17 @@ namespace { CSMWorld::UniversalId::Class_NonRecord, CSMWorld::UniversalId::Type_RegionMap, "Region Map", 0 }, { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Filters, "Filters", 0 }, + { CSMWorld::UniversalId::Class_ResourceList, CSMWorld::UniversalId::Type_Meshes, "Meshes", 0 }, + { CSMWorld::UniversalId::Class_ResourceList, CSMWorld::UniversalId::Type_Icons, "Icons", 0 }, + { CSMWorld::UniversalId::Class_ResourceList, CSMWorld::UniversalId::Type_Musics, "Music Files", 0 }, + { CSMWorld::UniversalId::Class_ResourceList, CSMWorld::UniversalId::Type_SoundsRes, "Sound Files", 0 }, + { CSMWorld::UniversalId::Class_ResourceList, CSMWorld::UniversalId::Type_Textures, "Textures", 0 }, + { CSMWorld::UniversalId::Class_ResourceList, CSMWorld::UniversalId::Type_Videos, "Videos", 0 }, + { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_DebugProfiles, "Debug Profiles", 0 }, + { CSMWorld::UniversalId::Class_Transient, CSMWorld::UniversalId::Type_RunLog, "Run Log", 0 }, + { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_SoundGens, "Sound Generators", 0 }, + { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_MagicEffects, "Magic Effects", 0 }, + { CSMWorld::UniversalId::Class_RecordList, CSMWorld::UniversalId::Type_Pathgrids, "Pathgrids", 0 }, { CSMWorld::UniversalId::Class_None, CSMWorld::UniversalId::Type_None, 0, 0 } // end marker }; @@ -61,8 +74,8 @@ namespace { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Spell, "Spell", ":./spell.png" }, { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Topic, "Topic", 0 }, { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Journal, "Journal", 0 }, - { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_TopicInfo, "TopicInfo", 0 }, - { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_JournalInfo, "JournalInfo", 0 }, + { CSMWorld::UniversalId::Class_SubRecord, CSMWorld::UniversalId::Type_TopicInfo, "TopicInfo", 0 }, + { CSMWorld::UniversalId::Class_SubRecord, CSMWorld::UniversalId::Type_JournalInfo, "JournalInfo", 0 }, { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Cell, "Cell", ":./cell.png" }, { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Cell_Missing, "Cell", ":./cell.png" }, { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Referenceable, "Referenceables", 0 }, @@ -90,10 +103,21 @@ namespace { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Static, "Static", ":./static.png" }, { CSMWorld::UniversalId::Class_RefRecord, CSMWorld::UniversalId::Type_Weapon, "Weapon", ":./weapon.png" }, { CSMWorld::UniversalId::Class_SubRecord, CSMWorld::UniversalId::Type_Reference, "Reference", 0 }, - { CSMWorld::UniversalId::Class_SubRecord, CSMWorld::UniversalId::Type_Filter, "Filter", ":./filter.png" }, + { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Filter, "Filter", ":./filter.png" }, { CSMWorld::UniversalId::Class_Collection, CSMWorld::UniversalId::Type_Scene, "Scene", 0 }, - { CSMWorld::UniversalId::Class_Collection, CSMWorld::UniversalId::Type_Preview, "Preview", 0 }, + { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Enchantment, "Enchantment", 0 }, + { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_BodyPart, "Body Part", 0 }, + { CSMWorld::UniversalId::Class_Resource, CSMWorld::UniversalId::Type_Mesh, "Mesh", 0 }, + { CSMWorld::UniversalId::Class_Resource, CSMWorld::UniversalId::Type_Icon, "Icon", 0 }, + { CSMWorld::UniversalId::Class_Resource, CSMWorld::UniversalId::Type_Music, "Music", 0 }, + { CSMWorld::UniversalId::Class_Resource, CSMWorld::UniversalId::Type_SoundRes, "Sound File", 0 }, + { CSMWorld::UniversalId::Class_Resource, CSMWorld::UniversalId::Type_Texture, "Texture", 0 }, + { CSMWorld::UniversalId::Class_Resource, CSMWorld::UniversalId::Type_Video, "Video", 0 }, + { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_DebugProfile, "Debug Profile", 0 }, + { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_SoundGen, "Sound Generator", 0 }, + { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_MagicEffect, "Magic Effect", 0 }, + { CSMWorld::UniversalId::Class_Record, CSMWorld::UniversalId::Type_Pathgrid, "Pathgrid", 0 }, { CSMWorld::UniversalId::Class_None, CSMWorld::UniversalId::Type_None, 0, 0 } // end marker }; @@ -304,7 +328,7 @@ std::string CSMWorld::UniversalId::getIcon() const for (int i=0; typeData[i].mName; ++i) if (typeData[i].mType==mType) - return typeData[i].mIcon ? typeData[i].mIcon : ""; + return typeData[i].mIcon ? typeData[i].mIcon : ":placeholder"; throw std::logic_error ("failed to retrieve UniversalId type icon"); } @@ -320,6 +344,29 @@ std::vector CSMWorld::UniversalId::listReferenceabl return list; } +CSMWorld::UniversalId::Type CSMWorld::UniversalId::getParentType (Type type) +{ + for (int i=0; sIdArg[i].mType; ++i) + if (type==sIdArg[i].mType) + { + if (sIdArg[i].mClass==Class_RefRecord) + return Type_Referenceables; + + if (sIdArg[i].mClass==Class_SubRecord || sIdArg[i].mClass==Class_Record || + sIdArg[i].mClass==Class_Resource) + { + if (type==Type_Cell_Missing) + return Type_Cells; + + return static_cast (type-1); + } + + break; + } + + return Type_None; +} + bool CSMWorld::operator== (const CSMWorld::UniversalId& left, const CSMWorld::UniversalId& right) { return left.isEqual (right); diff --git a/apps/opencs/model/world/universalid.hpp b/apps/opencs/model/world/universalid.hpp index 22779b263..ce2d021d0 100644 --- a/apps/opencs/model/world/universalid.hpp +++ b/apps/opencs/model/world/universalid.hpp @@ -22,7 +22,10 @@ namespace CSMWorld Class_RecordList, Class_Collection, // multiple types of records combined Class_Transient, // not part of the world data or the project data - Class_NonRecord // record like data that is not part of the world + Class_NonRecord, // record like data that is not part of the world + Class_Resource, ///< \attention Resource IDs are unique only within the + /// respective collection + Class_ResourceList }; enum ArgumentType @@ -32,6 +35,8 @@ namespace CSMWorld ArgumentType_Index }; + /// \note A record list type must always be immediately followed by the matching + /// record type, if this type is of class SubRecord or Record. enum Type { Type_None = 0, @@ -86,8 +91,8 @@ namespace CSMWorld Type_References, Type_Reference, Type_RegionMap, - Type_Filter, Type_Filters, + Type_Filter, Type_Topics, Type_Topic, Type_Journals, @@ -98,10 +103,35 @@ namespace CSMWorld Type_JournalInfo, Type_Scene, Type_Preview, - Type_LoadErrorLog + Type_LoadErrorLog, + Type_Enchantments, + Type_Enchantment, + Type_BodyParts, + Type_BodyPart, + Type_Meshes, + Type_Mesh, + Type_Icons, + Type_Icon, + Type_Musics, + Type_Music, + Type_SoundsRes, + Type_SoundRes, + Type_Textures, + Type_Texture, + Type_Videos, + Type_Video, + Type_DebugProfiles, + Type_DebugProfile, + Type_SoundGens, + Type_SoundGen, + Type_MagicEffects, + Type_MagicEffect, + Type_Pathgrids, + Type_Pathgrid, + Type_RunLog }; - enum { NumberOfTypes = Type_LoadErrorLog+1 }; + enum { NumberOfTypes = Type_RunLog+1 }; private: @@ -147,6 +177,11 @@ namespace CSMWorld ///< Will return an empty string, if no icon is available. static std::vector listReferenceableTypes(); + + /// If \a type is a SubRecord, RefRecord or Record type return the type of the table + /// that contains records of type \a type. + /// Otherwise return Type_None. + static Type getParentType (Type type); }; bool operator== (const UniversalId& left, const UniversalId& right); diff --git a/apps/opencs/view/doc/filedialog.cpp b/apps/opencs/view/doc/filedialog.cpp index ab56415a1..300656f33 100644 --- a/apps/opencs/view/doc/filedialog.cpp +++ b/apps/opencs/view/doc/filedialog.cpp @@ -18,7 +18,7 @@ #include "adjusterwidget.hpp" CSVDoc::FileDialog::FileDialog(QWidget *parent) : - QDialog(parent), mSelector (0), mFileWidget (0), mAdjusterWidget (0) + QDialog(parent), mSelector (0), mFileWidget (0), mAdjusterWidget (0), mDialogBuilt(false) { ui.setupUi (this); resize(400, 400); @@ -70,11 +70,15 @@ void CSVDoc::FileDialog::showDialog (ContentAction action) mAdjusterWidget->setFilenameCheck (mAction == ContentAction_New); - //connections common to both dialog view flavors - connect (mSelector, SIGNAL (signalCurrentGamefileIndexChanged (int)), - this, SLOT (slotUpdateAcceptButton (int))); + if(!mDialogBuilt) + { + //connections common to both dialog view flavors + connect (mSelector, SIGNAL (signalCurrentGamefileIndexChanged (int)), + this, SLOT (slotUpdateAcceptButton (int))); - connect (ui.projectButtonBox, SIGNAL (rejected()), this, SLOT (slotRejected())); + connect (ui.projectButtonBox, SIGNAL (rejected()), this, SLOT (slotRejected())); + mDialogBuilt = true; + } show(); raise(); @@ -85,22 +89,26 @@ void CSVDoc::FileDialog::buildNewFileView() { setWindowTitle(tr("Create a new addon")); - QPushButton* createButton = ui.projectButtonBox->button (QDialogButtonBox::Ok); - createButton->setText ("Create"); - createButton->setEnabled (false); + QPushButton* createButton = ui.projectButtonBox->button (QDialogButtonBox::Ok); + createButton->setText ("Create"); + createButton->setEnabled (false); - mFileWidget = new FileWidget (this); + if(!mFileWidget) + { + mFileWidget = new FileWidget (this); - mFileWidget->setType (true); - mFileWidget->extensionLabelIsVisible(true); + mFileWidget->setType (true); + mFileWidget->extensionLabelIsVisible(true); - ui.projectGroupBoxLayout->insertWidget (0, mFileWidget); + connect (mFileWidget, SIGNAL (nameChanged (const QString&, bool)), + mAdjusterWidget, SLOT (setName (const QString&, bool))); - connect (mFileWidget, SIGNAL (nameChanged (const QString&, bool)), - mAdjusterWidget, SLOT (setName (const QString&, bool))); + connect (mFileWidget, SIGNAL (nameChanged(const QString &, bool)), + this, SLOT (slotUpdateAcceptButton(const QString &, bool))); - connect (mFileWidget, SIGNAL (nameChanged(const QString &, bool)), - this, SLOT (slotUpdateAcceptButton(const QString &, bool))); + } + + ui.projectGroupBoxLayout->insertWidget (0, mFileWidget); connect (ui.projectButtonBox, SIGNAL (accepted()), this, SLOT (slotNewFile())); } @@ -109,13 +117,18 @@ void CSVDoc::FileDialog::buildOpenFileView() { setWindowTitle(tr("Open")); ui.projectGroupBox->setTitle (QString("")); + ui.projectButtonBox->button(QDialogButtonBox::Ok)->setText ("Open"); + if(mSelector->isGamefileSelected()) + ui.projectButtonBox->button(QDialogButtonBox::Ok)->setEnabled (true); + else + ui.projectButtonBox->button(QDialogButtonBox::Ok)->setEnabled (false); - ui.projectButtonBox->button(QDialogButtonBox::Ok)->setEnabled (false); - - connect (mSelector, SIGNAL (signalAddonFileSelected (int)), this, SLOT (slotUpdateAcceptButton (int))); - connect (mSelector, SIGNAL (signalAddonFileUnselected (int)), this, SLOT (slotUpdateAcceptButton (int))); - - connect (ui.projectButtonBox, SIGNAL (accepted()), this, SLOT (slotOpenFile())); + if(!mDialogBuilt) + { + connect (mSelector, SIGNAL (signalAddonFileSelected (int)), this, SLOT (slotUpdateAcceptButton (int))); + connect (mSelector, SIGNAL (signalAddonFileUnselected (int)), this, SLOT (slotUpdateAcceptButton (int))); + } + connect (ui.projectButtonBox, SIGNAL (accepted()), this, SLOT (slotOpenFile())); } void CSVDoc::FileDialog::slotUpdateAcceptButton (int) @@ -156,12 +169,26 @@ QString CSVDoc::FileDialog::filename() const void CSVDoc::FileDialog::slotRejected() { emit rejected(); + disconnect (ui.projectButtonBox, SIGNAL (accepted()), this, SLOT (slotNewFile())); + disconnect (ui.projectButtonBox, SIGNAL (accepted()), this, SLOT (slotOpenFile())); + if(mFileWidget) + { + delete mFileWidget; + mFileWidget = NULL; + } close(); } void CSVDoc::FileDialog::slotNewFile() { emit signalCreateNewFile (mAdjusterWidget->getPath()); + if(mFileWidget) + { + delete mFileWidget; + mFileWidget = NULL; + } + disconnect (ui.projectButtonBox, SIGNAL (accepted()), this, SLOT (slotNewFile())); + close(); } void CSVDoc::FileDialog::slotOpenFile() @@ -171,4 +198,6 @@ void CSVDoc::FileDialog::slotOpenFile() mAdjusterWidget->setName (file->filePath(), !file->isGameFile()); emit signalOpenFiles (mAdjusterWidget->getPath()); + disconnect (ui.projectButtonBox, SIGNAL (accepted()), this, SLOT (slotOpenFile())); + close(); } diff --git a/apps/opencs/view/doc/filedialog.hpp b/apps/opencs/view/doc/filedialog.hpp index d9fd56943..111cc0d80 100644 --- a/apps/opencs/view/doc/filedialog.hpp +++ b/apps/opencs/view/doc/filedialog.hpp @@ -37,6 +37,7 @@ namespace CSVDoc ContentAction mAction; FileWidget *mFileWidget; AdjusterWidget *mAdjusterWidget; + bool mDialogBuilt; public: diff --git a/apps/opencs/view/doc/filewidget.cpp b/apps/opencs/view/doc/filewidget.cpp index 9cd2fad42..f18fe695a 100644 --- a/apps/opencs/view/doc/filewidget.cpp +++ b/apps/opencs/view/doc/filewidget.cpp @@ -17,7 +17,7 @@ CSVDoc::FileWidget::FileWidget (QWidget *parent) : QWidget (parent), mAddon (fal QHBoxLayout *layout = new QHBoxLayout (this); mInput = new QLineEdit (this); - mInput->setValidator (new QRegExpValidator(QRegExp("^[a-zA-Z0-9\\s]*$"))); + mInput->setValidator (new QRegExpValidator(QRegExp("^[a-zA-Z0-9_-\\s]*$"))); layout->addWidget (mInput, 1); diff --git a/apps/opencs/view/doc/globaldebugprofilemenu.cpp b/apps/opencs/view/doc/globaldebugprofilemenu.cpp new file mode 100644 index 000000000..82bd96326 --- /dev/null +++ b/apps/opencs/view/doc/globaldebugprofilemenu.cpp @@ -0,0 +1,93 @@ + +#include "globaldebugprofilemenu.hpp" + +#include +#include + +#include + +#include "../../model/world/idtable.hpp" +#include "../../model/world/record.hpp" + +void CSVDoc::GlobalDebugProfileMenu::rebuild() +{ + clear(); + + delete mActions; + mActions = 0; + + int idColumn = mDebugProfiles->findColumnIndex (CSMWorld::Columns::ColumnId_Id); + int stateColumn = mDebugProfiles->findColumnIndex (CSMWorld::Columns::ColumnId_Modification); + int globalColumn = mDebugProfiles->findColumnIndex ( + CSMWorld::Columns::ColumnId_GlobalProfile); + + int size = mDebugProfiles->rowCount(); + + std::vector ids; + + for (int i=0; idata (mDebugProfiles->index (i, stateColumn)).toInt(); + + bool global = mDebugProfiles->data (mDebugProfiles->index (i, globalColumn)).toInt(); + + if (state!=CSMWorld::RecordBase::State_Deleted && global) + ids.push_back ( + mDebugProfiles->data (mDebugProfiles->index (i, idColumn)).toString()); + } + + mActions = new QActionGroup (this); + connect (mActions, SIGNAL (triggered (QAction *)), this, SLOT (actionTriggered (QAction *))); + + std::sort (ids.begin(), ids.end()); + + for (std::vector::const_iterator iter (ids.begin()); iter!=ids.end(); ++iter) + { + mActions->addAction (addAction (*iter)); + } +} + +CSVDoc::GlobalDebugProfileMenu::GlobalDebugProfileMenu (CSMWorld::IdTable *debugProfiles, + QWidget *parent) +: QMenu (parent), mDebugProfiles (debugProfiles), mActions (0) +{ + rebuild(); + + connect (mDebugProfiles, SIGNAL (rowsAboutToBeRemoved (const QModelIndex&, int, int)), + this, SLOT (profileAboutToBeRemoved (const QModelIndex&, int, int))); + + connect (mDebugProfiles, SIGNAL (rowsInserted (const QModelIndex&, int, int)), + this, SLOT (profileInserted (const QModelIndex&, int, int))); + + connect (mDebugProfiles, SIGNAL (dataChanged (const QModelIndex&, const QModelIndex&)), + this, SLOT (profileChanged (const QModelIndex&, const QModelIndex&))); +} + +void CSVDoc::GlobalDebugProfileMenu::updateActions (bool running) +{ + if (mActions) + mActions->setEnabled (!running); +} + +void CSVDoc::GlobalDebugProfileMenu::profileAboutToBeRemoved (const QModelIndex& parent, + int start, int end) +{ + rebuild(); +} + +void CSVDoc::GlobalDebugProfileMenu::profileInserted (const QModelIndex& parent, int start, + int end) +{ + rebuild(); +} + +void CSVDoc::GlobalDebugProfileMenu::profileChanged (const QModelIndex& topLeft, + const QModelIndex& bottomRight) +{ + rebuild(); +} + +void CSVDoc::GlobalDebugProfileMenu::actionTriggered (QAction *action) +{ + emit triggered (std::string (action->text().toUtf8().constData())); +} \ No newline at end of file diff --git a/apps/opencs/view/doc/globaldebugprofilemenu.hpp b/apps/opencs/view/doc/globaldebugprofilemenu.hpp new file mode 100644 index 000000000..0d7906cce --- /dev/null +++ b/apps/opencs/view/doc/globaldebugprofilemenu.hpp @@ -0,0 +1,49 @@ +#ifndef CSV_DOC_GLOBALDEBUGPROFILEMENU_H +#define CSV_DOC_GLOBALDEBUGPROFILEMENU_H + +#include + +class QModelIndex; +class QActionGroup; + +namespace CSMWorld +{ + class IdTable; +} + +namespace CSVDoc +{ + class GlobalDebugProfileMenu : public QMenu + { + Q_OBJECT + + CSMWorld::IdTable *mDebugProfiles; + QActionGroup *mActions; + + private: + + void rebuild(); + + public: + + GlobalDebugProfileMenu (CSMWorld::IdTable *debugProfiles, QWidget *parent = 0); + + void updateActions (bool running); + + private slots: + + void profileAboutToBeRemoved (const QModelIndex& parent, int start, int end); + + void profileInserted (const QModelIndex& parent, int start, int end); + + void profileChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight); + + void actionTriggered (QAction *action); + + signals: + + void triggered (const std::string& profile); + }; +} + +#endif diff --git a/apps/opencs/view/doc/loader.cpp b/apps/opencs/view/doc/loader.cpp index 7e4754ddf..ca7c93f9d 100644 --- a/apps/opencs/view/doc/loader.cpp +++ b/apps/opencs/view/doc/loader.cpp @@ -45,7 +45,7 @@ CSVDoc::LoadingDocument::LoadingDocument (CSMDoc::Document *document) mFileProgress->setValue (0); // record progress - mLayout->addWidget (new QLabel ("Records", this)); + mLayout->addWidget (mRecords = new QLabel ("Records", this)); mRecordProgress = new QProgressBar (this); @@ -75,22 +75,30 @@ CSVDoc::LoadingDocument::LoadingDocument (CSMDoc::Document *document) connect (mButtons, SIGNAL (rejected()), this, SLOT (cancel())); } -void CSVDoc::LoadingDocument::nextStage (const std::string& name, int steps) +void CSVDoc::LoadingDocument::nextStage (const std::string& name, int totalRecords) { mFile->setText (QString::fromUtf8 (("Loading: " + name).c_str())); mFileProgress->setValue (mFileProgress->value()+1); mRecordProgress->setValue (0); - mRecordProgress->setMaximum (steps>0 ? steps : 1); + mRecordProgress->setMaximum (totalRecords>0 ? totalRecords : 1); + + mTotalRecords = totalRecords; } -void CSVDoc::LoadingDocument::nextRecord() +void CSVDoc::LoadingDocument::nextRecord (int records) { - int value = mRecordProgress->value()+1; + if (records<=mTotalRecords) + { + mRecordProgress->setValue (records); + + std::ostringstream stream; - if (value<=mRecordProgress->maximum()) - mRecordProgress->setValue (value); + stream << "Records: " << records << " of " << mTotalRecords; + + mRecords->setText (QString::fromUtf8 (stream.str().c_str())); + } } void CSVDoc::LoadingDocument::abort (const std::string& error) @@ -168,20 +176,21 @@ void CSVDoc::Loader::loadingStopped (CSMDoc::Document *document, bool completed, } } -void CSVDoc::Loader::nextStage (CSMDoc::Document *document, const std::string& name, int steps) +void CSVDoc::Loader::nextStage (CSMDoc::Document *document, const std::string& name, + int totalRecords) { std::map::iterator iter = mDocuments.find (document); if (iter!=mDocuments.end()) - iter->second->nextStage (name, steps); + iter->second->nextStage (name, totalRecords); } -void CSVDoc::Loader::nextRecord (CSMDoc::Document *document) +void CSVDoc::Loader::nextRecord (CSMDoc::Document *document, int records) { std::map::iterator iter = mDocuments.find (document); if (iter!=mDocuments.end()) - iter->second->nextRecord(); + iter->second->nextRecord (records); } void CSVDoc::Loader::loadMessage (CSMDoc::Document *document, const std::string& message) diff --git a/apps/opencs/view/doc/loader.hpp b/apps/opencs/view/doc/loader.hpp index 69a8b48ba..e004007c9 100644 --- a/apps/opencs/view/doc/loader.hpp +++ b/apps/opencs/view/doc/loader.hpp @@ -26,6 +26,7 @@ namespace CSVDoc CSMDoc::Document *mDocument; QLabel *mFile; + QLabel *mRecords; QProgressBar *mFileProgress; QProgressBar *mRecordProgress; bool mAborted; @@ -33,6 +34,7 @@ namespace CSVDoc QLabel *mError; QListWidget *mMessages; QVBoxLayout *mLayout; + int mTotalRecords; private: @@ -42,9 +44,9 @@ namespace CSVDoc LoadingDocument (CSMDoc::Document *document); - void nextStage (const std::string& name, int steps); + void nextStage (const std::string& name, int totalRecords); - void nextRecord(); + void nextRecord (int records); void abort (const std::string& error); @@ -88,9 +90,9 @@ namespace CSVDoc void loadingStopped (CSMDoc::Document *document, bool completed, const std::string& error); - void nextStage (CSMDoc::Document *document, const std::string& name, int steps); + void nextStage (CSMDoc::Document *document, const std::string& name, int totalRecords); - void nextRecord (CSMDoc::Document *document); + void nextRecord (CSMDoc::Document *document, int records); void loadMessage (CSMDoc::Document *document, const std::string& message); }; diff --git a/apps/opencs/view/doc/runlogsubview.cpp b/apps/opencs/view/doc/runlogsubview.cpp new file mode 100644 index 000000000..68e888e8d --- /dev/null +++ b/apps/opencs/view/doc/runlogsubview.cpp @@ -0,0 +1,20 @@ + +#include "runlogsubview.hpp" + +#include + +CSVDoc::RunLogSubView::RunLogSubView (const CSMWorld::UniversalId& id, + CSMDoc::Document& document) +: SubView (id) +{ + QTextEdit *edit = new QTextEdit (this); + edit->setDocument (document.getRunLog()); + edit->setReadOnly (true); + + setWidget (edit); +} + +void CSVDoc::RunLogSubView::setEditLock (bool locked) +{ + // ignored since this SubView does not have editing +} \ No newline at end of file diff --git a/apps/opencs/view/doc/runlogsubview.hpp b/apps/opencs/view/doc/runlogsubview.hpp new file mode 100644 index 000000000..cfb676a37 --- /dev/null +++ b/apps/opencs/view/doc/runlogsubview.hpp @@ -0,0 +1,20 @@ +#ifndef CSV_DOC_RUNLOGSUBVIEW_H +#define CSV_DOC_RUNLOGSUBVIEW_H + +#include "subview.hpp" + +namespace CSVDoc +{ + class RunLogSubView : public SubView + { + Q_OBJECT + + public: + + RunLogSubView (const CSMWorld::UniversalId& id, CSMDoc::Document& document); + + virtual void setEditLock (bool locked); + }; +} + +#endif diff --git a/apps/opencs/view/doc/startup.cpp b/apps/opencs/view/doc/startup.cpp index 5d59492c6..799a07e14 100644 --- a/apps/opencs/view/doc/startup.cpp +++ b/apps/opencs/view/doc/startup.cpp @@ -81,6 +81,7 @@ QWidget *CSVDoc::StartupDialogue::createTools() config->setSizePolicy (QSizePolicy (QSizePolicy::Fixed, QSizePolicy::Fixed)); config->setIcon (QIcon (":startup/configure")); + config->setToolTip ("Open user settings"); layout->addWidget (config); diff --git a/apps/opencs/view/doc/subview.cpp b/apps/opencs/view/doc/subview.cpp index a80d21cb2..a399b5b5b 100644 --- a/apps/opencs/view/doc/subview.cpp +++ b/apps/opencs/view/doc/subview.cpp @@ -1,10 +1,14 @@ #include "subview.hpp" -CSVDoc::SubView::SubView (const CSMWorld::UniversalId& id) : mUniversalId (id) +#include "view.hpp" + +CSVDoc::SubView::SubView (const CSMWorld::UniversalId& id) + : mUniversalId (id) { /// \todo add a button to the title bar that clones this sub view setWindowTitle (QString::fromUtf8 (mUniversalId.toString().c_str())); + setAttribute(Qt::WA_DeleteOnClose); } CSMWorld::UniversalId CSVDoc::SubView::getUniversalId() const @@ -24,3 +28,18 @@ void CSVDoc::SubView::setUniversalId (const CSMWorld::UniversalId& id) mUniversalId = id; setWindowTitle (mUniversalId.toString().c_str()); } + +void CSVDoc::SubView::closeEvent (QCloseEvent *event) +{ + emit updateSubViewIndicies (this); +} + +std::string CSVDoc::SubView::getTitle() const +{ + return mUniversalId.toString(); +} + +void CSVDoc::SubView::closeRequest() +{ + emit closeRequest (this); +} diff --git a/apps/opencs/view/doc/subview.hpp b/apps/opencs/view/doc/subview.hpp index 733a75bb0..a8aa3cda1 100644 --- a/apps/opencs/view/doc/subview.hpp +++ b/apps/opencs/view/doc/subview.hpp @@ -18,6 +18,8 @@ namespace CSMWorld namespace CSVDoc { + class View; + class SubView : public QDockWidget { Q_OBJECT @@ -27,7 +29,9 @@ namespace CSVDoc // not implemented SubView (const SubView&); SubView& operator= (SubView&); + protected: + void setUniversalId(const CSMWorld::UniversalId& id); public: @@ -44,13 +48,27 @@ namespace CSVDoc virtual void useHint (const std::string& hint); ///< Default implementation: ignored + virtual std::string getTitle() const; + + virtual void updateUserSetting (const QString& name, const QStringList& value); + + private: + + void closeEvent (QCloseEvent *event); + signals: void focusId (const CSMWorld::UniversalId& universalId, const std::string& hint); - public slots: - virtual void updateUserSetting - (const QString &, const QStringList &); + void closeRequest (SubView *subView); + + void updateTitle(); + + void updateSubViewIndicies (SubView *view = 0); + + protected slots: + + void closeRequest(); }; } diff --git a/apps/opencs/view/doc/view.cpp b/apps/opencs/view/doc/view.cpp index e71b8435a..8b5efbea7 100644 --- a/apps/opencs/view/doc/view.cpp +++ b/apps/opencs/view/doc/view.cpp @@ -8,22 +8,34 @@ #include #include #include +#include #include "../../model/doc/document.hpp" #include "../../model/settings/usersettings.hpp" +#include "../../model/world/idtable.hpp" + #include "../world/subviews.hpp" +#include "../world/physicsmanager.hpp" #include "../tools/subviews.hpp" #include "viewmanager.hpp" #include "operations.hpp" #include "subview.hpp" +#include "globaldebugprofilemenu.hpp" +#include "runlogsubview.hpp" +#include "subviewfactoryimp.hpp" void CSVDoc::View::closeEvent (QCloseEvent *event) { if (!mViewManager.closeRequest (this)) event->ignore(); + else + { + // closeRequest() returns true if last document + mViewManager.removeDocAndView(mDocument); + } } void CSVDoc::View::setupFileMenu() @@ -93,6 +105,10 @@ void CSVDoc::View::setupViewMenu() mShowStatusBar = new QAction (tr ("Show Status Bar"), this); mShowStatusBar->setCheckable (true); connect (mShowStatusBar, SIGNAL (toggled (bool)), this, SLOT (toggleShowStatusBar (bool))); + std::string showStatusBar = + CSMSettings::UserSettings::instance().settingValue("window/show-statusbar").toStdString(); + if(showStatusBar == "true") + mShowStatusBar->setChecked(true); view->addAction (mShowStatusBar); QAction *filters = new QAction (tr ("Filters"), this); @@ -120,6 +136,10 @@ void CSVDoc::View::setupWorldMenu() connect (references, SIGNAL (triggered()), this, SLOT (addReferencesSubView())); world->addAction (references); + QAction *grid = new QAction (tr ("Pathgrid"), this); + connect (grid, SIGNAL (triggered()), this, SLOT (addPathgridSubView())); + world->addAction (grid); + world->addSeparator(); // items that don't represent single record lists follow here QAction *regionMap = new QAction (tr ("Region Map"), this); @@ -146,6 +166,14 @@ void CSVDoc::View::setupMechanicsMenu() QAction *spells = new QAction (tr ("Spells"), this); connect (spells, SIGNAL (triggered()), this, SLOT (addSpellsSubView())); mechanics->addAction (spells); + + QAction *enchantments = new QAction (tr ("Enchantments"), this); + connect (enchantments, SIGNAL (triggered()), this, SLOT (addEnchantmentsSubView())); + mechanics->addAction (enchantments); + + QAction *effects = new QAction (tr ("Magic Effects"), this); + connect (effects, SIGNAL (triggered()), this, SLOT (addMagicEffectsSubView())); + mechanics->addAction (effects); } void CSVDoc::View::setupCharacterMenu() @@ -187,6 +215,10 @@ void CSVDoc::View::setupCharacterMenu() QAction *journalInfos = new QAction (tr ("Journal Infos"), this); connect (journalInfos, SIGNAL (triggered()), this, SLOT (addJournalInfosSubView())); characters->addAction (journalInfos); + + QAction *bodyParts = new QAction (tr ("Body Parts"), this); + connect (bodyParts, SIGNAL (triggered()), this, SLOT (addBodyPartsSubView())); + characters->addAction (bodyParts); } void CSVDoc::View::setupAssetsMenu() @@ -196,6 +228,65 @@ void CSVDoc::View::setupAssetsMenu() QAction *sounds = new QAction (tr ("Sounds"), this); connect (sounds, SIGNAL (triggered()), this, SLOT (addSoundsSubView())); assets->addAction (sounds); + + QAction *soundGens = new QAction (tr ("Sound Generators"), this); + connect (soundGens, SIGNAL (triggered()), this, SLOT (addSoundGensSubView())); + assets->addAction (soundGens); + + assets->addSeparator(); // resources follow here + + QAction *meshes = new QAction (tr ("Meshes"), this); + connect (meshes, SIGNAL (triggered()), this, SLOT (addMeshesSubView())); + assets->addAction (meshes); + + QAction *icons = new QAction (tr ("Icons"), this); + connect (icons, SIGNAL (triggered()), this, SLOT (addIconsSubView())); + assets->addAction (icons); + + QAction *musics = new QAction (tr ("Music"), this); + connect (musics, SIGNAL (triggered()), this, SLOT (addMusicsSubView())); + assets->addAction (musics); + + QAction *soundsRes = new QAction (tr ("Sound Files"), this); + connect (soundsRes, SIGNAL (triggered()), this, SLOT (addSoundsResSubView())); + assets->addAction (soundsRes); + + QAction *textures = new QAction (tr ("Textures"), this); + connect (textures, SIGNAL (triggered()), this, SLOT (addTexturesSubView())); + assets->addAction (textures); + + QAction *videos = new QAction (tr ("Videos"), this); + connect (videos, SIGNAL (triggered()), this, SLOT (addVideosSubView())); + assets->addAction (videos); +} + +void CSVDoc::View::setupDebugMenu() +{ + QMenu *debug = menuBar()->addMenu (tr ("Debug")); + + QAction *profiles = new QAction (tr ("Debug Profiles"), this); + connect (profiles, SIGNAL (triggered()), this, SLOT (addDebugProfilesSubView())); + debug->addAction (profiles); + + debug->addSeparator(); + + mGlobalDebugProfileMenu = new GlobalDebugProfileMenu ( + &dynamic_cast (*mDocument->getData().getTableModel ( + CSMWorld::UniversalId::Type_DebugProfiles)), this); + + connect (mGlobalDebugProfileMenu, SIGNAL (triggered (const std::string&)), + this, SLOT (run (const std::string&))); + + QAction *runDebug = debug->addMenu (mGlobalDebugProfileMenu); + runDebug->setText (tr ("Run OpenMW")); + + mStopDebug = new QAction (tr ("Shutdown OpenMW"), this); + connect (mStopDebug, SIGNAL (triggered()), this, SLOT (stop())); + debug->addAction (mStopDebug); + + QAction *runLog = new QAction (tr ("Run Log"), this); + connect (runLog, SIGNAL (triggered()), this, SLOT (addRunLogSubView())); + debug->addAction (runLog); } void CSVDoc::View::setupUi() @@ -207,6 +298,7 @@ void CSVDoc::View::setupUi() setupMechanicsMenu(); setupCharacterMenu(); setupAssetsMenu(); + setupDebugMenu(); } void CSVDoc::View::updateTitle() @@ -221,12 +313,51 @@ void CSVDoc::View::updateTitle() if (mViewTotal>1) stream << " [" << (mViewIndex+1) << "/" << mViewTotal << "]"; + CSMSettings::UserSettings &userSettings = CSMSettings::UserSettings::instance(); + + bool hideTitle = userSettings.setting ("window/hide-subview", QString ("false"))=="true" && + mSubViews.size()==1 && !mSubViews.at (0)->isFloating(); + + if (hideTitle) + stream << " - " << mSubViews.at (0)->getTitle(); + setWindowTitle (stream.str().c_str()); } +void CSVDoc::View::updateSubViewIndicies(SubView *view) +{ + if(view && mSubViews.contains(view)) + mSubViews.removeOne(view); + + CSMSettings::UserSettings &userSettings = CSMSettings::UserSettings::instance(); + + bool hideTitle = userSettings.setting ("window/hide-subview", QString ("false"))=="true" && + mSubViews.size()==1 && !mSubViews.at (0)->isFloating(); + + updateTitle(); + + foreach (SubView *subView, mSubViews) + { + if (!subView->isFloating()) + { + if (hideTitle) + { + subView->setTitleBarWidget (new QWidget (this)); + subView->setWindowTitle (QString::fromUtf8 (subView->getTitle().c_str())); + } + else + { + delete subView->titleBarWidget(); + subView->setTitleBarWidget (0); + } + } + } +} + void CSVDoc::View::updateActions() { bool editing = !(mDocument->getState() & CSMDoc::State_Locked); + bool running = mDocument->getState() & CSMDoc::State_Running; for (std::vector::iterator iter (mEditingActions.begin()); iter!=mEditingActions.end(); ++iter) (*iter)->setEnabled (editing); @@ -234,8 +365,11 @@ void CSVDoc::View::updateActions() mUndo->setEnabled (editing & mDocument->getUndoStack().canUndo()); mRedo->setEnabled (editing & mDocument->getUndoStack().canRedo()); - mSave->setEnabled (!(mDocument->getState() & CSMDoc::State_Saving)); + mSave->setEnabled (!(mDocument->getState() & CSMDoc::State_Saving) && !running); mVerify->setEnabled (!(mDocument->getState() & CSMDoc::State_Verifying)); + + mGlobalDebugProfileMenu->updateActions (running); + mStopDebug->setEnabled (running); } CSVDoc::View::View (ViewManager& viewManager, CSMDoc::Document *document, int totalViews) @@ -243,12 +377,16 @@ CSVDoc::View::View (ViewManager& viewManager, CSMDoc::Document *document, int to mViewTotal (totalViews) { QString width = CSMSettings::UserSettings::instance().settingValue - ("Window Size/Width"); + ("window/default-width"); QString height = CSMSettings::UserSettings::instance().settingValue - ("Window Size/Height"); + ("window/default-height"); - resize (width.toInt(), height.toInt()); + // trick to get the window decorations and their sizes + show(); + hide(); + resize (width.toInt() - (frameGeometry().width() - geometry().width()), + height.toInt() - (frameGeometry().height() - geometry().height())); mSubViewWindow.setDockOptions (QMainWindow::AllowNestedDocks); @@ -261,10 +399,16 @@ CSVDoc::View::View (ViewManager& viewManager, CSMDoc::Document *document, int to setupUi(); + updateActions(); + CSVWorld::addSubViewFactories (mSubViewFactory); CSVTools::addSubViewFactories (mSubViewFactory); + mSubViewFactory.add (CSMWorld::UniversalId::Type_RunLog, new SubViewFactory); + connect (mOperations, SIGNAL (abortOperation (int)), this, SLOT (abortOperation (int))); + + CSVWorld::PhysicsManager::instance()->setupPhysics(document); } CSVDoc::View::~View() @@ -318,38 +462,73 @@ void CSVDoc::View::updateProgress (int current, int max, int type, int threads) void CSVDoc::View::addSubView (const CSMWorld::UniversalId& id, const std::string& hint) { - /// \todo add an user setting for limiting the number of sub views per top level view. Automatically open a new top level view if this - /// number is exceeded + CSMSettings::UserSettings &userSettings = CSMSettings::UserSettings::instance(); - /// \todo if the sub view limit setting is one, the sub view title bar should be hidden and the text in the main title bar adjusted - /// accordingly + const std::vector referenceables(CSMWorld::UniversalId::listReferenceableTypes()); + bool isReferenceable = std::find(referenceables.begin(), referenceables.end(), id.getType()) != referenceables.end(); - /// \todo add an user setting to reuse sub views (on a per document basis or on a per top level view basis) + // User setting to reuse sub views (on a per top level view basis) + bool reuse = + userSettings.setting ("window/reuse", QString("true")) == "true" ? true : false; + if(reuse) + { + foreach(SubView *sb, mSubViews) + { + if((isReferenceable && (CSMWorld::UniversalId(CSMWorld::UniversalId::Type_Referenceable, id.getId()) == CSMWorld::UniversalId(CSMWorld::UniversalId::Type_Referenceable, sb->getUniversalId().getId()))) + || (!isReferenceable && (id == sb->getUniversalId()))) + { + sb->setFocus(Qt::OtherFocusReason); // FIXME: focus not quite working + return; + } + } + } + + // User setting for limiting the number of sub views per top level view. + // Automatically open a new top level view if this number is exceeded + // + // If the sub view limit setting is one, the sub view title bar is hidden and the + // text in the main title bar is adjusted accordingly + int maxSubView = userSettings.setting("window/max-subviews", QString("256")).toInt(); + if(mSubViews.size() >= maxSubView) // create a new top level view + { + mViewManager.addView(mDocument, id, hint); + + return; + } - const std::vector referenceables(CSMWorld::UniversalId::listReferenceableTypes()); SubView *view = NULL; if(std::find(referenceables.begin(), referenceables.end(), id.getType()) != referenceables.end()) { view = mSubViewFactory.makeSubView (CSMWorld::UniversalId(CSMWorld::UniversalId::Type_Referenceable, id.getId()), *mDocument); - } else + } + else { view = mSubViewFactory.makeSubView (id, *mDocument); } assert(view); + view->setParent(this); + mSubViews.append(view); // only after assert if (!hint.empty()) view->useHint (hint); + int minWidth = userSettings.setting ("window/minimum-width", QString("325")).toInt(); + view->setMinimumWidth(minWidth); + view->setStatusBar (mShowStatusBar->isChecked()); mSubViewWindow.addDockWidget (Qt::TopDockWidgetArea, view); + updateSubViewIndicies(); + connect (view, SIGNAL (focusId (const CSMWorld::UniversalId&, const std::string&)), this, SLOT (addSubView (const CSMWorld::UniversalId&, const std::string&))); - connect (&CSMSettings::UserSettings::instance(), - SIGNAL (userSettingUpdated (const QString &, const QStringList &)), - view, - SLOT (updateUserSetting (const QString &, const QStringList &))); + connect (view, SIGNAL (closeRequest (SubView *)), this, SLOT (closeRequest (SubView *))); + + connect (view, SIGNAL (updateTitle()), this, SLOT (updateTitle())); + + connect (view, SIGNAL (updateSubViewIndicies (SubView *)), + this, SLOT (updateSubViewIndicies (SubView *))); view->show(); } @@ -469,6 +648,71 @@ void CSVDoc::View::addJournalInfosSubView() addSubView (CSMWorld::UniversalId::Type_JournalInfos); } +void CSVDoc::View::addEnchantmentsSubView() +{ + addSubView (CSMWorld::UniversalId::Type_Enchantments); +} + +void CSVDoc::View::addBodyPartsSubView() +{ + addSubView (CSMWorld::UniversalId::Type_BodyParts); +} + +void CSVDoc::View::addSoundGensSubView() +{ + addSubView (CSMWorld::UniversalId::Type_SoundGens); +} + +void CSVDoc::View::addMeshesSubView() +{ + addSubView (CSMWorld::UniversalId::Type_Meshes); +} + +void CSVDoc::View::addIconsSubView() +{ + addSubView (CSMWorld::UniversalId::Type_Icons); +} + +void CSVDoc::View::addMusicsSubView() +{ + addSubView (CSMWorld::UniversalId::Type_Musics); +} + +void CSVDoc::View::addSoundsResSubView() +{ + addSubView (CSMWorld::UniversalId::Type_SoundsRes); +} + +void CSVDoc::View::addMagicEffectsSubView() +{ + addSubView (CSMWorld::UniversalId::Type_MagicEffects); +} + +void CSVDoc::View::addTexturesSubView() +{ + addSubView (CSMWorld::UniversalId::Type_Textures); +} + +void CSVDoc::View::addVideosSubView() +{ + addSubView (CSMWorld::UniversalId::Type_Videos); +} + +void CSVDoc::View::addDebugProfilesSubView() +{ + addSubView (CSMWorld::UniversalId::Type_DebugProfiles); +} + +void CSVDoc::View::addRunLogSubView() +{ + addSubView (CSMWorld::UniversalId::Type_RunLog); +} + +void CSVDoc::View::addPathgridSubView() +{ + addSubView (CSMWorld::UniversalId::Type_Pathgrids); +} + void CSVDoc::View::abortOperation (int type) { mDocument->abortOperation (type); @@ -497,9 +741,16 @@ void CSVDoc::View::resizeViewHeight (int height) resize (geometry().width(), height); } -void CSVDoc::View::updateUserSetting - (const QString &name, const QStringList &list) -{} +void CSVDoc::View::updateUserSetting (const QString &name, const QStringList &list) +{ + if (name=="window/hide-subview") + updateSubViewIndicies (0); + + foreach (SubView *subView, mSubViews) + { + subView->updateUserSetting (name, list); + } +} void CSVDoc::View::toggleShowStatusBar (bool show) { @@ -510,7 +761,33 @@ void CSVDoc::View::toggleShowStatusBar (bool show) } } +void CSVDoc::View::toggleStatusBar(bool checked) +{ + mShowStatusBar->setChecked(checked); +} + void CSVDoc::View::loadErrorLog() { addSubView (CSMWorld::UniversalId (CSMWorld::UniversalId::Type_LoadErrorLog, 0)); } + +void CSVDoc::View::run (const std::string& profile, const std::string& startupInstruction) +{ + mDocument->startRunning (profile, startupInstruction); +} + +void CSVDoc::View::stop() +{ + mDocument->stopRunning(); +} + +void CSVDoc::View::closeRequest (SubView *subView) +{ + CSMSettings::UserSettings &userSettings = CSMSettings::UserSettings::instance(); + + if (mSubViews.size()>1 || mViewTotal<=1 || + userSettings.setting ("window/hide-subview", QString ("false"))!="true") + subView->deleteLater(); + else if (mViewManager.closeRequest (this)) + mViewManager.removeDocAndView (mDocument); +} diff --git a/apps/opencs/view/doc/view.hpp b/apps/opencs/view/doc/view.hpp index 686c001dc..55ea5ee51 100644 --- a/apps/opencs/view/doc/view.hpp +++ b/apps/opencs/view/doc/view.hpp @@ -25,6 +25,7 @@ namespace CSVDoc { class ViewManager; class Operations; + class GlobalDebugProfileMenu; class View : public QMainWindow { @@ -34,15 +35,18 @@ namespace CSVDoc CSMDoc::Document *mDocument; int mViewIndex; int mViewTotal; + QList mSubViews; QAction *mUndo; QAction *mRedo; QAction *mSave; QAction *mVerify; QAction *mShowStatusBar; + QAction *mStopDebug; std::vector mEditingActions; Operations *mOperations; SubViewFactoryManager mSubViewFactory; QMainWindow mSubViewWindow; + GlobalDebugProfileMenu *mGlobalDebugProfileMenu; // not implemented @@ -67,9 +71,9 @@ namespace CSVDoc void setupAssetsMenu(); - void setupUi(); + void setupDebugMenu(); - void updateTitle(); + void setupUi(); void updateActions(); @@ -101,6 +105,8 @@ namespace CSVDoc void updateProgress (int current, int max, int type, int threads); + void toggleStatusBar(bool checked); + Operations *getOperations() const; /// Function called by view manager when user preferences are updated @@ -128,6 +134,11 @@ namespace CSVDoc void updateUserSetting (const QString &, const QStringList &); + void updateTitle(); + + // called when subviews are added or removed + void updateSubViewIndicies (SubView *view = 0); + private slots: void newView(); @@ -178,9 +189,41 @@ namespace CSVDoc void addJournalInfosSubView(); + void addEnchantmentsSubView(); + + void addBodyPartsSubView(); + + void addSoundGensSubView(); + + void addMagicEffectsSubView(); + + void addMeshesSubView(); + + void addIconsSubView(); + + void addMusicsSubView(); + + void addSoundsResSubView(); + + void addTexturesSubView(); + + void addVideosSubView(); + + void addDebugProfilesSubView(); + + void addRunLogSubView(); + + void addPathgridSubView(); + void toggleShowStatusBar (bool show); void loadErrorLog(); + + void run (const std::string& profile, const std::string& startupInstruction = ""); + + void stop(); + + void closeRequest (SubView *subView); }; } diff --git a/apps/opencs/view/doc/viewmanager.cpp b/apps/opencs/view/doc/viewmanager.cpp index 02f6a5467..c4fd66884 100644 --- a/apps/opencs/view/doc/viewmanager.cpp +++ b/apps/opencs/view/doc/viewmanager.cpp @@ -9,12 +9,14 @@ #include "../../model/doc/documentmanager.hpp" #include "../../model/doc/document.hpp" #include "../../model/world/columns.hpp" +#include "../../model/world/universalid.hpp" #include "../world/util.hpp" #include "../world/enumdelegate.hpp" #include "../world/vartypedelegate.hpp" #include "../world/recordstatusdelegate.hpp" #include "../world/idtypedelegate.hpp" +#include "../world/physicsmanager.hpp" #include "../../model/settings/usersettings.hpp" @@ -78,7 +80,12 @@ CSVDoc::ViewManager::ViewManager (CSMDoc::DocumentManager& documentManager) { CSMWorld::ColumnBase::Display_WeaponType, CSMWorld::Columns::ColumnId_WeaponType, false }, { CSMWorld::ColumnBase::Display_DialogueType, CSMWorld::Columns::ColumnId_DialogueType, false }, { CSMWorld::ColumnBase::Display_QuestStatusType, CSMWorld::Columns::ColumnId_QuestStatusType, false }, - { CSMWorld::ColumnBase::Display_Gender, CSMWorld::Columns::ColumnId_Gender, true } + { CSMWorld::ColumnBase::Display_EnchantmentType, CSMWorld::Columns::ColumnId_EnchantmentType, false }, + { CSMWorld::ColumnBase::Display_BodyPartType, CSMWorld::Columns::ColumnId_BodyPartType, false }, + { CSMWorld::ColumnBase::Display_MeshType, CSMWorld::Columns::ColumnId_MeshType, false }, + { CSMWorld::ColumnBase::Display_Gender, CSMWorld::Columns::ColumnId_Gender, true }, + { CSMWorld::ColumnBase::Display_SoundGeneratorType, CSMWorld::Columns::ColumnId_SoundGeneratorType, false }, + { CSMWorld::ColumnBase::Display_School, CSMWorld::Columns::ColumnId_School, true } }; for (std::size_t i=0; itoggleStatusBar (showStatusBar == "true"); view->show(); connect (view, SIGNAL (newGameRequest ()), this, SIGNAL (newGameRequest())); @@ -154,6 +165,14 @@ CSVDoc::View *CSVDoc::ViewManager::addView (CSMDoc::Document *document) return view; } +CSVDoc::View *CSVDoc::ViewManager::addView (CSMDoc::Document *document, const CSMWorld::UniversalId& id, const std::string& hint) +{ + View* view = addView(document); + view->addSubView(id, hint); + + return view; +} + int CSVDoc::ViewManager::countViews (const CSMDoc::Document *document) const { int count = 0; @@ -169,7 +188,7 @@ bool CSVDoc::ViewManager::closeRequest (View *view) { std::vector::iterator iter = std::find (mViews.begin(), mViews.end(), view); - bool continueWithClose = true; + bool continueWithClose = false; if (iter!=mViews.end()) { @@ -189,6 +208,25 @@ bool CSVDoc::ViewManager::closeRequest (View *view) return continueWithClose; } +// NOTE: This method assumes that it is called only if the last document +void CSVDoc::ViewManager::removeDocAndView (CSMDoc::Document *document) +{ + for (std::vector::iterator iter (mViews.begin()); iter!=mViews.end(); ++iter) + { + // the first match should also be the only match + if((*iter)->getDocument() == document) + { + mDocumentManager.removeDocument(document); + (*iter)->deleteLater(); + mViews.erase (iter); + CSVWorld::PhysicsManager::instance()->removeDocument(document); + + updateIndices(); + return; + } + } +} + bool CSVDoc::ViewManager::notifySaveOnClose (CSVDoc::View *view) { bool result = true; @@ -207,13 +245,19 @@ bool CSVDoc::ViewManager::notifySaveOnClose (CSVDoc::View *view) bool CSVDoc::ViewManager::showModifiedDocumentMessageBox (CSVDoc::View *view) { - QMessageBox messageBox; + emit closeMessageBox(); + + QMessageBox messageBox(view); CSMDoc::Document *document = view->getDocument(); + messageBox.setWindowTitle (document->getSavePath().filename().string().c_str()); messageBox.setText ("The document has been modified."); messageBox.setInformativeText ("Do you want to save your changes?"); messageBox.setStandardButtons (QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel); messageBox.setDefaultButton (QMessageBox::Save); + messageBox.setWindowModality (Qt::NonModal); + messageBox.hide(); + messageBox.show(); bool retVal = true; @@ -338,8 +382,40 @@ void CSVDoc::ViewManager::onExitWarningHandler (int state, CSMDoc::Document *doc } } +bool CSVDoc::ViewManager::removeDocument (CSVDoc::View *view) +{ + if(!notifySaveOnClose(view)) + return false; + else + { + // don't bother closing views or updating indicies, but remove from mViews + CSMDoc::Document * document = view->getDocument(); + std::vector remainingViews; + std::vector::const_iterator iter = mViews.begin(); + for (; iter!=mViews.end(); ++iter) + { + if(document == (*iter)->getDocument()) + (*iter)->setVisible(false); + else + remainingViews.push_back(*iter); + } + mDocumentManager.removeDocument(document); + mViews = remainingViews; + } + return true; +} + void CSVDoc::ViewManager::exitApplication (CSVDoc::View *view) { - if (notifySaveOnClose (view)) - QApplication::instance()->exit(); + if(!removeDocument(view)) // close the current document first + return; + + while(!mViews.empty()) // attempt to close all other documents + { + mViews.back()->activateWindow(); + mViews.back()->raise(); // raise the window to alert the user + if(!removeDocument(mViews.back())) + return; + } + // Editor exits (via a signal) when the last document is deleted } diff --git a/apps/opencs/view/doc/viewmanager.hpp b/apps/opencs/view/doc/viewmanager.hpp index 8cc92774b..cdd5ac768 100644 --- a/apps/opencs/view/doc/viewmanager.hpp +++ b/apps/opencs/view/doc/viewmanager.hpp @@ -18,6 +18,11 @@ namespace CSVWorld class CommandDelegateFactoryCollection; } +namespace CSMWorld +{ + class UniversalId; +} + namespace CSVDoc { class View; @@ -41,6 +46,7 @@ namespace CSVDoc bool notifySaveOnClose (View *view = 0); bool showModifiedDocumentMessageBox (View *view); bool showSaveInProgressMessageBox (View *view); + bool removeDocument(View *view); public: @@ -51,10 +57,13 @@ namespace CSVDoc View *addView (CSMDoc::Document *document); ///< The ownership of the returned view is not transferred. + View *addView (CSMDoc::Document *document, const CSMWorld::UniversalId& id, const std::string& hint); + int countViews (const CSMDoc::Document *document) const; ///< Return number of views for \a document. bool closeRequest (View *view); + void removeDocAndView (CSMDoc::Document *document); signals: diff --git a/apps/opencs/view/filter/filterbox.cpp b/apps/opencs/view/filter/filterbox.cpp index e588770b1..7a42ef0a5 100644 --- a/apps/opencs/view/filter/filterbox.cpp +++ b/apps/opencs/view/filter/filterbox.cpp @@ -35,7 +35,11 @@ void CSVFilter::FilterBox::setRecordFilter (const std::string& filter) void CSVFilter::FilterBox::dropEvent (QDropEvent* event) { - std::vector data = dynamic_cast (event->mimeData())->getData(); + const CSMWorld::TableMimeData* mime = dynamic_cast (event->mimeData()); + if (!mime) // May happen when non-records (e.g. plain text) are dragged and dropped + return; + + std::vector data = mime->getData(); emit recordDropped(data, event->proposedAction()); } @@ -54,4 +58,4 @@ void CSVFilter::FilterBox::createFilterRequest (std::vector< std::pair< std::str Qt::DropAction action) { mRecordFilterBox->createFilterRequest(filterSource, action); -} \ No newline at end of file +} diff --git a/apps/opencs/view/filter/filtercreator.cpp b/apps/opencs/view/filter/filtercreator.cpp deleted file mode 100644 index 640c9fe78..000000000 --- a/apps/opencs/view/filter/filtercreator.cpp +++ /dev/null @@ -1,77 +0,0 @@ - -#include "filtercreator.hpp" - -#include -#include - -#include "../../model/filter/filter.hpp" - -#include "../../model/world/data.hpp" -#include "../../model/world/commands.hpp" -#include "../../model/world/columns.hpp" -#include "../../model/world/idtable.hpp" - -std::string CSVFilter::FilterCreator::getNamespace() const -{ - switch (mScope->currentIndex()) - { - case CSMFilter::Filter::Scope_Project: return "project::"; - case CSMFilter::Filter::Scope_Session: return "session::"; - } - - return ""; -} - -void CSVFilter::FilterCreator::update() -{ - mNamespace->setText (QString::fromUtf8 (getNamespace().c_str())); - GenericCreator::update(); -} - -std::string CSVFilter::FilterCreator::getId() const -{ - return getNamespace() + GenericCreator::getId(); -} - -void CSVFilter::FilterCreator::configureCreateCommand (CSMWorld::CreateCommand& command) const -{ - int index = - dynamic_cast (*getData().getTableModel (getCollectionId())). - findColumnIndex (CSMWorld::Columns::ColumnId_Scope); - - command.addValue (index, mScope->currentIndex()); -} - -CSVFilter::FilterCreator::FilterCreator (CSMWorld::Data& data, QUndoStack& undoStack, - const CSMWorld::UniversalId& id) -: GenericCreator (data, undoStack, id) -{ - mNamespace = new QLabel ("::", this); - insertAtBeginning (mNamespace, false); - - mScope = new QComboBox (this); - - mScope->addItem ("Project"); - mScope->addItem ("Session"); - /// \todo re-enable for OpenMW 1.1 - // mScope->addItem ("Content"); - - connect (mScope, SIGNAL (currentIndexChanged (int)), this, SLOT (setScope (int))); - - insertAtBeginning (mScope, false); - - QLabel *label = new QLabel ("Scope", this); - insertAtBeginning (label, false); - - mScope->setCurrentIndex (1); -} - -void CSVFilter::FilterCreator::reset() -{ - GenericCreator::reset(); -} - -void CSVFilter::FilterCreator::setScope (int index) -{ - update(); -} diff --git a/apps/opencs/view/filter/filtercreator.hpp b/apps/opencs/view/filter/filtercreator.hpp deleted file mode 100644 index 437d01c8d..000000000 --- a/apps/opencs/view/filter/filtercreator.hpp +++ /dev/null @@ -1,43 +0,0 @@ -#ifndef CSV_FILTER_FILTERCREATOR_H -#define CSV_FILTER_FILTERCREATOR_H - -class QComboBox; -class QLabel; - -#include "../world/genericcreator.hpp" - -namespace CSVFilter -{ - class FilterCreator : public CSVWorld::GenericCreator - { - Q_OBJECT - - QComboBox *mScope; - QLabel *mNamespace; - - private: - - std::string getNamespace() const; - - protected: - - void update(); - - virtual std::string getId() const; - - virtual void configureCreateCommand (CSMWorld::CreateCommand& command) const; - - public: - - FilterCreator (CSMWorld::Data& data, QUndoStack& undoStack, - const CSMWorld::UniversalId& id); - - virtual void reset(); - - private slots: - - void setScope (int index); - }; -} - -#endif diff --git a/apps/opencs/view/filter/recordfilterbox.cpp b/apps/opencs/view/filter/recordfilterbox.cpp index ec5647618..97490d508 100644 --- a/apps/opencs/view/filter/recordfilterbox.cpp +++ b/apps/opencs/view/filter/recordfilterbox.cpp @@ -11,9 +11,11 @@ CSVFilter::RecordFilterBox::RecordFilterBox (CSMWorld::Data& data, QWidget *pare { QHBoxLayout *layout = new QHBoxLayout (this); - layout->setContentsMargins (0, 0, 0, 0); + layout->setContentsMargins (0, 6, 5, 0); - layout->addWidget (new QLabel ("Record Filter", this)); + QLabel *label = new QLabel("Record Filter", this); + label->setIndent(2); + layout->addWidget (label); mEdit = new EditWidget (data, this); diff --git a/apps/opencs/view/render/cell.cpp b/apps/opencs/view/render/cell.cpp new file mode 100644 index 000000000..75e11cc10 --- /dev/null +++ b/apps/opencs/view/render/cell.cpp @@ -0,0 +1,238 @@ + +#include "cell.hpp" + +#include +#include + +#include +#include + +#include "../../model/world/idtable.hpp" +#include "../../model/world/columns.hpp" +#include "../../model/world/data.hpp" +#include "../world/physicssystem.hpp" + +#include "elements.hpp" +#include "terrainstorage.hpp" + +bool CSVRender::Cell::removeObject (const std::string& id) +{ + std::map::iterator iter = + mObjects.find (Misc::StringUtils::lowerCase (id)); + + if (iter==mObjects.end()) + return false; + + delete iter->second; + mObjects.erase (iter); + return true; +} + +bool CSVRender::Cell::addObjects (int start, int end) +{ + CSMWorld::IdTable& references = dynamic_cast ( + *mData.getTableModel (CSMWorld::UniversalId::Type_References)); + + int idColumn = references.findColumnIndex (CSMWorld::Columns::ColumnId_Id); + int cellColumn = references.findColumnIndex (CSMWorld::Columns::ColumnId_Cell); + int stateColumn = references.findColumnIndex (CSMWorld::Columns::ColumnId_Modification); + + bool modified = false; + + for (int i=start; i<=end; ++i) + { + std::string cell = Misc::StringUtils::lowerCase (references.data ( + references.index (i, cellColumn)).toString().toUtf8().constData()); + + int state = references.data (references.index (i, stateColumn)).toInt(); + + if (cell==mId && state!=CSMWorld::RecordBase::State_Deleted) + { + std::string id = Misc::StringUtils::lowerCase (references.data ( + references.index (i, idColumn)).toString().toUtf8().constData()); + + mObjects.insert (std::make_pair (id, new Object (mData, mCellNode, id, false, mPhysics))); + modified = true; + } + } + + return modified; +} + +CSVRender::Cell::Cell (CSMWorld::Data& data, Ogre::SceneManager *sceneManager, + const std::string& id, CSVWorld::PhysicsSystem *physics, const Ogre::Vector3& origin) +: mData (data), mId (Misc::StringUtils::lowerCase (id)), mSceneMgr(sceneManager), mPhysics(physics) +{ + mCellNode = sceneManager->getRootSceneNode()->createChildSceneNode(); + mCellNode->setPosition (origin); + + CSMWorld::IdTable& references = dynamic_cast ( + *mData.getTableModel (CSMWorld::UniversalId::Type_References)); + + int rows = references.rowCount(); + + addObjects (0, rows-1); + + const CSMWorld::IdCollection& land = mData.getLand(); + int landIndex = land.searchId(mId); + if (landIndex != -1) + { + mTerrain.reset(new Terrain::TerrainGrid(sceneManager, new TerrainStorage(mData), Element_Terrain, true, + Terrain::Align_XY)); + + const ESM::Land* esmLand = land.getRecord(mId).get().mLand.get(); + mTerrain->loadCell(esmLand->mX, + esmLand->mY); + + if(esmLand) + { + float verts = ESM::Land::LAND_SIZE; + float worldsize = ESM::Land::REAL_SIZE; + mX = esmLand->mX; + mY = esmLand->mY; + mPhysics->addHeightField(sceneManager, + esmLand->mLandData->mHeights, mX, mY, 0, worldsize / (verts-1), verts); + } + } +} + +CSVRender::Cell::~Cell() +{ + mPhysics->removeHeightField(mSceneMgr, mX, mY); + + for (std::map::iterator iter (mObjects.begin()); + iter!=mObjects.end(); ++iter) + delete iter->second; + + mCellNode->getCreator()->destroySceneNode (mCellNode); +} + +bool CSVRender::Cell::referenceableDataChanged (const QModelIndex& topLeft, + const QModelIndex& bottomRight) +{ + bool modified = false; + + for (std::map::iterator iter (mObjects.begin()); + iter!=mObjects.end(); ++iter) + if (iter->second->referenceableDataChanged (topLeft, bottomRight)) + modified = true; + + return modified; +} + +bool CSVRender::Cell::referenceableAboutToBeRemoved (const QModelIndex& parent, int start, + int end) +{ + if (parent.isValid()) + return false; + + bool modified = false; + + for (std::map::iterator iter (mObjects.begin()); + iter!=mObjects.end(); ++iter) + if (iter->second->referenceableAboutToBeRemoved (parent, start, end)) + modified = true; + + return modified; +} + +bool CSVRender::Cell::referenceDataChanged (const QModelIndex& topLeft, + const QModelIndex& bottomRight) +{ + CSMWorld::IdTable& references = dynamic_cast ( + *mData.getTableModel (CSMWorld::UniversalId::Type_References)); + + int idColumn = references.findColumnIndex (CSMWorld::Columns::ColumnId_Id); + int cellColumn = references.findColumnIndex (CSMWorld::Columns::ColumnId_Cell); + int stateColumn = references.findColumnIndex (CSMWorld::Columns::ColumnId_Modification); + + // list IDs in cell + std::map ids; // id, deleted state + + for (int i=topLeft.row(); i<=bottomRight.row(); ++i) + { + std::string cell = Misc::StringUtils::lowerCase (references.data ( + references.index (i, cellColumn)).toString().toUtf8().constData()); + + if (cell==mId) + { + std::string id = Misc::StringUtils::lowerCase (references.data ( + references.index (i, idColumn)).toString().toUtf8().constData()); + + int state = references.data (references.index (i, stateColumn)).toInt(); + + ids.insert (std::make_pair (id, state==CSMWorld::RecordBase::State_Deleted)); + } + } + + // perform update and remove where needed + bool modified = false; + + for (std::map::iterator iter (mObjects.begin()); + iter!=mObjects.end(); ++iter) + { + if (iter->second->referenceDataChanged (topLeft, bottomRight)) + modified = true; + + std::map::iterator iter2 = ids.find (iter->first); + + if (iter2!=ids.end()) + { + if (iter2->second) + { + removeObject (iter->first); + modified = true; + } + + ids.erase (iter2); + } + } + + // add new objects + for (std::map::iterator iter (ids.begin()); iter!=ids.end(); ++iter) + { + mObjects.insert (std::make_pair ( + iter->first, new Object (mData, mCellNode, iter->first, false, mPhysics))); + + modified = true; + } + + return modified; +} + +bool CSVRender::Cell::referenceAboutToBeRemoved (const QModelIndex& parent, int start, + int end) +{ + if (parent.isValid()) + return false; + + CSMWorld::IdTable& references = dynamic_cast ( + *mData.getTableModel (CSMWorld::UniversalId::Type_References)); + + int idColumn = references.findColumnIndex (CSMWorld::Columns::ColumnId_Id); + + bool modified = false; + + for (int row = start; row<=end; ++row) + if (removeObject (references.data ( + references.index (row, idColumn)).toString().toUtf8().constData())) + modified = true; + + return modified; +} + +bool CSVRender::Cell::referenceAdded (const QModelIndex& parent, int start, int end) +{ + if (parent.isValid()) + return false; + + return addObjects (start, end); +} + +float CSVRender::Cell::getTerrainHeightAt(const Ogre::Vector3 &pos) const +{ + if(mTerrain.get() != NULL) + return mTerrain->getHeightAt(pos); + else + return -std::numeric_limits::max(); +} diff --git a/apps/opencs/view/render/cell.hpp b/apps/opencs/view/render/cell.hpp new file mode 100644 index 000000000..d38f0c68d --- /dev/null +++ b/apps/opencs/view/render/cell.hpp @@ -0,0 +1,88 @@ +#ifndef OPENCS_VIEW_CELL_H +#define OPENCS_VIEW_CELL_H + +#include +#include +#include + +#include + +#include + +#include "object.hpp" + +class QModelIndex; + +namespace Ogre +{ + class SceneManager; + class SceneNode; +} + +namespace CSMWorld +{ + class Data; +} + +namespace CSVWorld +{ + class PhysicsSystem; +} + +namespace CSVRender +{ + class Cell + { + CSMWorld::Data& mData; + std::string mId; + Ogre::SceneNode *mCellNode; + std::map mObjects; + std::auto_ptr mTerrain; + CSVWorld::PhysicsSystem *mPhysics; + Ogre::SceneManager *mSceneMgr; + int mX; + int mY; + + /// Ignored if cell does not have an object with the given ID. + /// + /// \return Was the object deleted? + bool removeObject (const std::string& id); + + /// Add objects from reference table that are within this cell. + /// + /// \return Have any objects been added? + bool addObjects (int start, int end); + + public: + + Cell (CSMWorld::Data& data, Ogre::SceneManager *sceneManager, const std::string& id, + CSVWorld::PhysicsSystem *physics, const Ogre::Vector3& origin = Ogre::Vector3 (0, 0, 0)); + + ~Cell(); + + /// \return Did this call result in a modification of the visual representation of + /// this cell? + bool referenceableDataChanged (const QModelIndex& topLeft, + const QModelIndex& bottomRight); + + /// \return Did this call result in a modification of the visual representation of + /// this cell? + bool referenceableAboutToBeRemoved (const QModelIndex& parent, int start, int end); + + /// \return Did this call result in a modification of the visual representation of + /// this cell? + bool referenceDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight); + + /// \return Did this call result in a modification of the visual representation of + /// this cell? + bool referenceAboutToBeRemoved (const QModelIndex& parent, int start, int end); + + /// \return Did this call result in a modification of the visual representation of + /// this cell? + bool referenceAdded (const QModelIndex& parent, int start, int end); + + float getTerrainHeightAt(const Ogre::Vector3 &pos) const; + }; +} + +#endif diff --git a/apps/opencs/view/render/editmode.cpp b/apps/opencs/view/render/editmode.cpp new file mode 100644 index 000000000..51a137d3b --- /dev/null +++ b/apps/opencs/view/render/editmode.cpp @@ -0,0 +1,19 @@ + +#include "editmode.hpp" + +#include "worldspacewidget.hpp" + +CSVRender::EditMode::EditMode (WorldspaceWidget *worldspaceWidget, const QIcon& icon, + unsigned int mask, const QString& tooltip, QWidget *parent) +: ModeButton (icon, tooltip, parent), mWorldspaceWidget (worldspaceWidget), mMask (mask) +{} + +unsigned int CSVRender::EditMode::getInteractionMask() const +{ + return mMask; +} + +void CSVRender::EditMode::activate (CSVWidget::SceneToolbar *toolbar) +{ + mWorldspaceWidget->setInteractionMask (mMask); +} \ No newline at end of file diff --git a/apps/opencs/view/render/editmode.hpp b/apps/opencs/view/render/editmode.hpp new file mode 100644 index 000000000..c3192f8ea --- /dev/null +++ b/apps/opencs/view/render/editmode.hpp @@ -0,0 +1,28 @@ +#ifndef CSV_RENDER_EDITMODE_H +#define CSV_RENDER_EDITMODE_H + +#include "../widget/modebutton.hpp" + +namespace CSVRender +{ + class WorldspaceWidget; + + class EditMode : public CSVWidget::ModeButton + { + Q_OBJECT + + WorldspaceWidget *mWorldspaceWidget; + unsigned int mMask; + + public: + + EditMode (WorldspaceWidget *worldspaceWidget, const QIcon& icon, unsigned int mask, + const QString& tooltip = "", QWidget *parent = 0); + + unsigned int getInteractionMask() const; + + virtual void activate (CSVWidget::SceneToolbar *toolbar); + }; +} + +#endif diff --git a/apps/opencs/view/render/elements.hpp b/apps/opencs/view/render/elements.hpp new file mode 100644 index 000000000..784e41212 --- /dev/null +++ b/apps/opencs/view/render/elements.hpp @@ -0,0 +1,23 @@ +#ifndef CSV_RENDER_ELEMENTS_H +#define CSV_RENDER_ELEMENTS_H + +namespace CSVRender +{ + /// Visual elements in a scene + enum Elements + { + // elements that are part of the actual scene + Element_Reference = 0x1, + Element_Terrain = 0x2, + Element_Water = 0x4, + Element_Pathgrid = 0x8, + Element_Fog = 0x10, + + // control elements + Element_CellMarker = 0x10000, + Element_CellArrow = 0x20000, + Element_CellBorder = 0x40000 + }; +} + +#endif diff --git a/apps/opencs/view/render/mousestate.cpp b/apps/opencs/view/render/mousestate.cpp new file mode 100644 index 000000000..45e846f74 --- /dev/null +++ b/apps/opencs/view/render/mousestate.cpp @@ -0,0 +1,463 @@ +#include "mousestate.hpp" + +#include +#include +#include +#include + +#include +#include +#include + +#include "../../model/settings/usersettings.hpp" +#include "../../model/world/commands.hpp" +#include "../../model/world/idtable.hpp" +#include "../../model/world/universalid.hpp" +#include "../world/physicssystem.hpp" + +#include "elements.hpp" +#include "worldspacewidget.hpp" + +namespace CSVRender +{ + // mouse picking + // FIXME: need to virtualise mouse buttons + // + // State machine: + // + // [default] mousePressEvent->check if the mouse is pointing at an object + // if yes, create collision planes then go to [grab] + // else check for terrain + // + // [grab] mouseReleaseEvent->if same button and new obj, go to [edit] + // mouseMoveEvent->if same button, go to [drag] + // other mouse events or buttons, go back to [default] (i.e. like 'cancel') + // + // [drag] mouseReleaseEvent->if same button, place the object at the new + // location, update the document then go to [edit] + // mouseMoveEvent->update position to the user based on ray to the collision + // planes and render the object at the new location, but do not update + // the document yet + // + // [edit] TODO, probably fine positional adjustments or rotations; clone/delete? + // + // + // press press (obj) + // [default] --------> [grab] <-------------------- [edit] + // ^ (obj) | | ------> [drag] -----> ^ + // | | | move ^ | release | + // | | | | | | + // | | | +-+ | + // | | | move | + // +----------------+ +--------------------------+ + // release release + // (same obj) (new obj) + // + // + + MouseState::MouseState(WorldspaceWidget *parent) + : mParent(parent), mPhysics(parent->getPhysics()), mSceneManager(parent->getSceneManager()) + , mCurrentObj(""), mMouseState(Mouse_Default), mOldPos(0,0), mMouseEventTimer(0), mPlane(0) + , mGrabbedSceneNode(""), mOrigObjPos(Ogre::Vector3()), mOrigMousePos(Ogre::Vector3()) + , mCurrentMousePos(Ogre::Vector3()), mOffset(0.0f) + , mColIndexPosX(0), mColIndexPosY(0), mColIndexPosZ(0), mIdTableModel(0) + { + const CSMWorld::RefCollection& references = mParent->mDocument.getData().getReferences(); + + mColIndexPosX = references.findColumnIndex(CSMWorld::Columns::ColumnId_PositionXPos); + mColIndexPosY = references.findColumnIndex(CSMWorld::Columns::ColumnId_PositionYPos); + mColIndexPosZ = references.findColumnIndex(CSMWorld::Columns::ColumnId_PositionZPos); + + mIdTableModel = static_cast( + mParent->mDocument.getData().getTableModel(CSMWorld::UniversalId::Type_Reference)); + + mMouseEventTimer = new QElapsedTimer(); + mMouseEventTimer->invalidate(); + + std::pair planeRes = planeAxis(); + mPlane = new Ogre::Plane(planeRes.first, 0); + Ogre::MeshPtr mesh = Ogre::MeshManager::getSingleton().createPlane("mouse", + Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME, + *mPlane, + 300000,300000, // FIXME: use far clip dist? + 1,1, // segments + true, // normals + 1, // numTexCoordSets + 1,1, // uTile, vTile + planeRes.second // upVector + ); + } + + MouseState::~MouseState () + { + delete mMouseEventTimer; + delete mPlane; + } + + void MouseState::mouseMoveEvent (QMouseEvent *event) + { + switch(mMouseState) + { + case Mouse_Grab: + { + // check if min elapsed time to stop false detection of drag + if(!mMouseEventTimer->isValid() || !mMouseEventTimer->hasExpired(100)) // ms + break; + + mMouseEventTimer->invalidate(); + mMouseState = Mouse_Drag; + + /* FALL_THROUGH */ + } + case Mouse_Drag: + { + if(event->pos() != mOldPos) // TODO: maybe don't update less than a quantum? + { + mOldPos = event->pos(); + + // ray test against the plane to provide feedback to the user the + // relative movement of the object on the x-y plane + std::pair planeResult = mousePositionOnPlane(event->pos(), *mPlane); + if(planeResult.first) + { + if(mGrabbedSceneNode != "") + { + std::pair planeRes = planeAxis(); + Ogre::Vector3 pos = mOrigObjPos + planeRes.first*mOffset; + mSceneManager->getSceneNode(mGrabbedSceneNode)->setPosition(pos+planeResult.second-mOrigMousePos); + mCurrentMousePos = planeResult.second; + mPhysics->moveSceneNodes(mGrabbedSceneNode, pos+planeResult.second-mOrigMousePos); + updateSceneWidgets(); + } + } + } + break; + } + case Mouse_Edit: + case Mouse_Default: + { + break; // error event, ignore + } + /* NO_DEFAULT_CASE */ + } + } + + void MouseState::mousePressEvent (QMouseEvent *event) + { + switch(mMouseState) + { + case Mouse_Grab: + case Mouse_Drag: + { + break; + } + case Mouse_Edit: + case Mouse_Default: + { + if(event->buttons() & Qt::RightButton) + { + std::pair result = objectUnderCursor(event->x(), event->y()); + if(result.first == "") + break; + + mGrabbedSceneNode = result.first; + // ray test agaist the plane to get a starting position of the + // mouse in relation to the object position + std::pair planeRes = planeAxis(); + mPlane->redefine(planeRes.first, result.second); + std::pair planeResult = mousePositionOnPlane(event->pos(), *mPlane); + if(planeResult.first) + { + mOrigMousePos = planeResult.second; + mCurrentMousePos = planeResult.second; + mOffset = 0.0f; + } + + mOrigObjPos = mSceneManager->getSceneNode(mGrabbedSceneNode)->getPosition(); + mMouseEventTimer->start(); + + mMouseState = Mouse_Grab; + } + break; + } + /* NO_DEFAULT_CASE */ + } + } + + void MouseState::mouseReleaseEvent (QMouseEvent *event) + { + switch(mMouseState) + { + case Mouse_Grab: + { + std::pair result = objectUnderCursor(event->x(), event->y()); + if(result.first != "") + { + if(result.first == mCurrentObj) + { + // unselect object + mMouseState = Mouse_Default; + mCurrentObj = ""; + } + else + { + // select object + mMouseState = Mouse_Edit; + mCurrentObj = result.first; + + } + } + break; + } + case Mouse_Drag: + { + // final placement + std::pair planeResult = mousePositionOnPlane(event->pos(), *mPlane); + if(planeResult.first) + { + if(mGrabbedSceneNode != "") + { + std::pair planeRes = planeAxis(); + Ogre::Vector3 pos = mOrigObjPos+planeRes.first*mOffset+planeResult.second-mOrigMousePos; + // use the saved scene node name since the physics model has not moved yet + std::string referenceId = mPhysics->sceneNodeToRefId(mGrabbedSceneNode); + + mParent->mDocument.getUndoStack().beginMacro (QObject::tr("Move Object")); + mParent->mDocument.getUndoStack().push(new CSMWorld::ModifyCommand(*mIdTableModel, + mIdTableModel->getModelIndex(referenceId, mColIndexPosX), pos.x)); + mParent->mDocument.getUndoStack().push(new CSMWorld::ModifyCommand(*mIdTableModel, + mIdTableModel->getModelIndex(referenceId, mColIndexPosY), pos.y)); + mParent->mDocument.getUndoStack().push(new CSMWorld::ModifyCommand(*mIdTableModel, + mIdTableModel->getModelIndex(referenceId, mColIndexPosZ), pos.z)); + mParent->mDocument.getUndoStack().endMacro(); + + // FIXME: highlight current object? + //mCurrentObj = mGrabbedSceneNode; // FIXME: doesn't work? + mCurrentObj = ""; // whether the object is selected + + mMouseState = Mouse_Edit; + + // reset states + mCurrentMousePos = Ogre::Vector3(); // mouse pos to use in wheel event + mOrigMousePos = Ogre::Vector3(); // starting pos of mouse in world space + mOrigObjPos = Ogre::Vector3(); // starting pos of object in world space + mGrabbedSceneNode = ""; // id of the object + mOffset = 0.0f; // used for z-axis movement + mOldPos = QPoint(0, 0); // to calculate relative movement of mouse + } + } + break; + } + case Mouse_Edit: + case Mouse_Default: + { + // probably terrain, check + std::pair result = terrainUnderCursor(event->x(), event->y()); + if(result.first != "") + { + // FIXME: terrain editing goes here + } + break; + } + /* NO_DEFAULT_CASE */ + } + mMouseEventTimer->invalidate(); + } + + void MouseState::mouseDoubleClickEvent (QMouseEvent *event) + { + event->ignore(); + //mPhysics->toggleDebugRendering(mSceneManager); + //mParent->flagAsModified(); + } + + bool MouseState::wheelEvent (QWheelEvent *event) + { + switch(mMouseState) + { + case Mouse_Grab: + mMouseState = Mouse_Drag; + + /* FALL_THROUGH */ + case Mouse_Drag: + { + // move the object along the z axis during Mouse_Drag or Mouse_Grab + if (event->delta()) + { + // seems positive is up and negative is down + mOffset += (event->delta()/1); // FIXME: arbitrary number, make config option? + + std::pair planeRes = planeAxis(); + Ogre::Vector3 pos = mOrigObjPos + planeRes.first*mOffset; + mSceneManager->getSceneNode(mGrabbedSceneNode)->setPosition(pos+mCurrentMousePos-mOrigMousePos); + mPhysics->moveSceneNodes(mGrabbedSceneNode, pos+mCurrentMousePos-mOrigMousePos); + updateSceneWidgets(); + } + break; + } + case Mouse_Edit: + case Mouse_Default: + { + return false; + } + /* NO_DEFAULT_CASE */ + } + + return true; + } + + void MouseState::cancelDrag() + { + switch(mMouseState) + { + case Mouse_Grab: + case Mouse_Drag: + { + // cancel operation & return the object to the original position + mSceneManager->getSceneNode(mGrabbedSceneNode)->setPosition(mOrigObjPos); + // update all SceneWidgets and their SceneManagers + mPhysics->moveSceneNodes(mGrabbedSceneNode, mOrigObjPos); + updateSceneWidgets(); + + // reset states + mMouseState = Mouse_Default; + mCurrentMousePos = Ogre::Vector3(); + mOrigMousePos = Ogre::Vector3(); + mOrigObjPos = Ogre::Vector3(); + mGrabbedSceneNode = ""; + mCurrentObj = ""; + mOldPos = QPoint(0, 0); + mMouseEventTimer->invalidate(); + mOffset = 0.0f; + + break; + } + case Mouse_Edit: + case Mouse_Default: + { + break; + } + /* NO_DEFAULT_CASE */ + } + } + + //plane Z, upvector Y, mOffset z : x-y plane, wheel up/down + //plane Y, upvector X, mOffset y : y-z plane, wheel left/right + //plane X, upvector Y, mOffset x : x-z plane, wheel closer/further + std::pair MouseState::planeAxis() + { + bool screenCoord = true; + Ogre::Vector3 dir = getCamera()->getDerivedDirection(); + + QString wheelDir = "Closer/Further"; + if(wheelDir == "Left/Right") + { + if(screenCoord) + return std::make_pair(getCamera()->getDerivedRight(), getCamera()->getDerivedUp()); + else + return std::make_pair(Ogre::Vector3::UNIT_Y, Ogre::Vector3::UNIT_Z); + } + else if(wheelDir == "Up/Down") + { + if(screenCoord) + return std::make_pair(getCamera()->getDerivedUp(), Ogre::Vector3(-dir.x, -dir.y, -dir.z)); + else + return std::make_pair(Ogre::Vector3::UNIT_Z, Ogre::Vector3::UNIT_X); + } + else + { + if(screenCoord) + return std::make_pair(Ogre::Vector3(-dir.x, -dir.y, -dir.z), getCamera()->getDerivedRight()); + else + return std::make_pair(Ogre::Vector3::UNIT_X, Ogre::Vector3::UNIT_Y); + } + } + + std::pair MouseState::mousePositionOnPlane(const QPoint &pos, const Ogre::Plane &plane) + { + // using a really small value seems to mess up with the projections + float nearClipDistance = getCamera()->getNearClipDistance(); // save existing + getCamera()->setNearClipDistance(10.0f); // arbitrary number + Ogre::Ray mouseRay = getCamera()->getCameraToViewportRay( + (float) pos.x() / getViewport()->getActualWidth(), + (float) pos.y() / getViewport()->getActualHeight()); + getCamera()->setNearClipDistance(nearClipDistance); // restore + std::pair planeResult = mouseRay.intersects(plane); + + if(planeResult.first) + return std::make_pair(true, mouseRay.getPoint(planeResult.second)); + else + return std::make_pair(false, Ogre::Vector3()); // should only happen if the plane is too small + } + + std::pair MouseState::terrainUnderCursor(const int mouseX, const int mouseY) + { + if(!getViewport()) + return std::make_pair("", Ogre::Vector3()); + + float x = (float) mouseX / getViewport()->getActualWidth(); + float y = (float) mouseY / getViewport()->getActualHeight(); + + std::pair result = mPhysics->castRay(x, y, mSceneManager, getCamera()); + if(result.first != "") + { + // FIXME: is there a better way to distinguish terrain from objects? + QString name = QString(result.first.c_str()); + if(name.contains(QRegExp("^HeightField"))) + { + return result; + } + } + + return std::make_pair("", Ogre::Vector3()); + } + + std::pair MouseState::objectUnderCursor(const int mouseX, const int mouseY) + { + if(!getViewport()) + return std::make_pair("", Ogre::Vector3()); + + float x = (float) mouseX / getViewport()->getActualWidth(); + float y = (float) mouseY / getViewport()->getActualHeight(); + + std::pair result = mPhysics->castRay(x, y, mSceneManager, getCamera()); + if(result.first != "") + { + // NOTE: anything not terrain is assumed to be an object + QString name = QString(result.first.c_str()); + if(!name.contains(QRegExp("^HeightField"))) + { + uint32_t visibilityMask = getViewport()->getVisibilityMask(); + bool ignoreObjects = !(visibilityMask & (uint32_t)CSVRender::Element_Reference); + + if(!ignoreObjects && mSceneManager->hasSceneNode(result.first)) + { + return result; + } + } + } + + return std::make_pair("", Ogre::Vector3()); + } + + void MouseState::updateSceneWidgets() + { + std::map sceneWidgets = mPhysics->sceneWidgets(); + + std::map::iterator iter = sceneWidgets.begin(); + for(; iter != sceneWidgets.end(); ++iter) + { + (*iter).second->updateScene(); + } + } + + Ogre::Camera *MouseState::getCamera() + { + return mParent->getCamera(); + } + + Ogre::Viewport *MouseState::getViewport() + { + return mParent->getCamera()->getViewport(); + } +} diff --git a/apps/opencs/view/render/mousestate.hpp b/apps/opencs/view/render/mousestate.hpp new file mode 100644 index 000000000..27907bb33 --- /dev/null +++ b/apps/opencs/view/render/mousestate.hpp @@ -0,0 +1,90 @@ +#ifndef OPENCS_VIEW_MOUSESTATE_H +#define OPENCS_VIEW_MOUSESTATE_H + +#include +#include +#include + +class QElapsedTimer; +class QMouseEvent; +class QWheelEvent; + +namespace Ogre +{ + class Plane; + class SceneManager; + class Camera; + class Viewport; +} + +namespace CSVWorld +{ + class PhysicsSystem; +} + +namespace CSMWorld +{ + class IdTable; +} + +namespace CSVRender +{ + class WorldspaceWidget; + + class MouseState + { + enum MouseStates + { + Mouse_Grab, + Mouse_Drag, + Mouse_Edit, + Mouse_Default + }; + MouseStates mMouseState; + + WorldspaceWidget *mParent; + CSVWorld::PhysicsSystem *mPhysics; // local copy + Ogre::SceneManager *mSceneManager; // local copy + + QPoint mOldPos; + std::string mCurrentObj; + std::string mGrabbedSceneNode; + QElapsedTimer *mMouseEventTimer; + Ogre::Plane *mPlane; + Ogre::Vector3 mOrigObjPos; + Ogre::Vector3 mOrigMousePos; + Ogre::Vector3 mCurrentMousePos; + float mOffset; + + CSMWorld::IdTable *mIdTableModel; + int mColIndexPosX; + int mColIndexPosY; + int mColIndexPosZ; + + public: + + MouseState(WorldspaceWidget *parent); + ~MouseState(); + + void mouseMoveEvent (QMouseEvent *event); + void mousePressEvent (QMouseEvent *event); + void mouseReleaseEvent (QMouseEvent *event); + void mouseDoubleClickEvent (QMouseEvent *event); + bool wheelEvent (QWheelEvent *event); + + void cancelDrag(); + + private: + + std::pair mousePositionOnPlane(const QPoint &pos, const Ogre::Plane &plane); + std::pair terrainUnderCursor(const int mouseX, const int mouseY); + std::pair objectUnderCursor(const int mouseX, const int mouseY); + std::pair planeAxis(); + void updateSceneWidgets(); + + Ogre::Camera *getCamera(); // friend access + Ogre::Viewport *getViewport(); // friend access + }; +} + +#endif // OPENCS_VIEW_MOUSESTATE_H diff --git a/apps/opencs/view/render/navigation.cpp b/apps/opencs/view/render/navigation.cpp index 14ae7f0b7..705759104 100644 --- a/apps/opencs/view/render/navigation.cpp +++ b/apps/opencs/view/render/navigation.cpp @@ -11,9 +11,14 @@ float CSVRender::Navigation::getFactor (bool mouse) const return factor; } +CSVRender::Navigation::Navigation() + : mFastModeFactor(1) +{ +} + CSVRender::Navigation::~Navigation() {} void CSVRender::Navigation::setFastModeFactor (float factor) { mFastModeFactor = factor; -} \ No newline at end of file +} diff --git a/apps/opencs/view/render/navigation.hpp b/apps/opencs/view/render/navigation.hpp index 873519609..ead8f3e13 100644 --- a/apps/opencs/view/render/navigation.hpp +++ b/apps/opencs/view/render/navigation.hpp @@ -20,6 +20,7 @@ namespace CSVRender public: + Navigation(); virtual ~Navigation(); void setFastModeFactor (float factor); diff --git a/apps/opencs/view/render/navigation1st.cpp b/apps/opencs/view/render/navigation1st.cpp index 91f88634a..5d9a03468 100644 --- a/apps/opencs/view/render/navigation1st.cpp +++ b/apps/opencs/view/render/navigation1st.cpp @@ -44,10 +44,11 @@ bool CSVRender::Navigation1st::mouseMoved (const QPoint& delta, int mode) float deltaPitch = getFactor (true) * delta.y(); Ogre::Radian newPitch = oldPitch + Ogre::Degree (deltaPitch); - Ogre::Radian limit (Ogre::Math::PI/2-0.5); - - if ((deltaPitch>0 && newPitch-limit)) + if ((deltaPitch>0 && newPitchOgre::Radian(0.5))) + { mCamera->pitch (Ogre::Degree (deltaPitch)); + } } return true; diff --git a/apps/opencs/view/render/object.cpp b/apps/opencs/view/render/object.cpp new file mode 100644 index 000000000..21219db8f --- /dev/null +++ b/apps/opencs/view/render/object.cpp @@ -0,0 +1,242 @@ + +#include "object.hpp" + +#include +#include +#include + +#include "../../model/world/data.hpp" +#include "../../model/world/ref.hpp" +#include "../../model/world/refidcollection.hpp" + +#include "../world/physicssystem.hpp" + +#include "elements.hpp" + +void CSVRender::Object::clearSceneNode (Ogre::SceneNode *node) +{ + for (Ogre::SceneNode::ObjectIterator iter = node->getAttachedObjectIterator(); + iter.hasMoreElements(); ) + { + Ogre::MovableObject* object = dynamic_cast (iter.getNext()); + node->getCreator()->destroyMovableObject (object); + } + + for (Ogre::SceneNode::ChildNodeIterator iter = node->getChildIterator(); + iter.hasMoreElements(); ) + { + Ogre::SceneNode* childNode = dynamic_cast (iter.getNext()); + clearSceneNode (childNode); + node->getCreator()->destroySceneNode (childNode); + } +} + +void CSVRender::Object::clear() +{ + mObject.setNull(); + + clearSceneNode (mBase); +} + +void CSVRender::Object::update() +{ + if(!mObject.isNull()) + mPhysics->removePhysicsObject(mBase->getName()); + + clear(); + + std::string model; + int error = 0; // 1 referemceanƶe does not exist, 2 referenceable does not specify a mesh + + const CSMWorld::RefIdCollection& referenceables = mData.getReferenceables(); + + int index = referenceables.searchId (mReferenceableId); + + if (index==-1) + error = 1; + else + { + /// \todo check for Deleted state (error 1) + + model = referenceables.getData (index, + referenceables.findColumnIndex (CSMWorld::Columns::ColumnId_Model)). + toString().toUtf8().constData(); + + if (model.empty()) + error = 2; + } + + if (error) + { + Ogre::Entity* entity = mBase->getCreator()->createEntity (Ogre::SceneManager::PT_CUBE); + entity->setMaterialName("BaseWhite"); /// \todo adjust material according to error + entity->setVisibilityFlags (Element_Reference); + + mBase->attachObject (entity); + } + else + { + mObject = NifOgre::Loader::createObjects (mBase, "Meshes\\" + model); + mObject->setVisibilityFlags (Element_Reference); + + if (mPhysics && !mReferenceId.empty()) + { + const CSMWorld::CellRef& reference = getReference(); + + // position + Ogre::Vector3 position; + if (!mForceBaseToZero) + position = Ogre::Vector3(reference.mPos.pos[0], reference.mPos.pos[1], reference.mPos.pos[2]); + + // orientation + Ogre::Quaternion xr (Ogre::Radian (-reference.mPos.rot[0]), Ogre::Vector3::UNIT_X); + Ogre::Quaternion yr (Ogre::Radian (-reference.mPos.rot[1]), Ogre::Vector3::UNIT_Y); + Ogre::Quaternion zr (Ogre::Radian (-reference.mPos.rot[2]), Ogre::Vector3::UNIT_Z); + + mPhysics->addObject("meshes\\" + model, mBase->getName(), mReferenceId, reference.mScale, position, xr*yr*zr); + } + } +} + +void CSVRender::Object::adjust() +{ + if (mReferenceId.empty()) + return; + + const CSMWorld::CellRef& reference = getReference(); + + // position + if (!mForceBaseToZero) + mBase->setPosition (Ogre::Vector3 ( + reference.mPos.pos[0], reference.mPos.pos[1], reference.mPos.pos[2])); + + // orientation + Ogre::Quaternion xr (Ogre::Radian (-reference.mPos.rot[0]), Ogre::Vector3::UNIT_X); + + Ogre::Quaternion yr (Ogre::Radian (-reference.mPos.rot[1]), Ogre::Vector3::UNIT_Y); + + Ogre::Quaternion zr (Ogre::Radian (-reference.mPos.rot[2]), Ogre::Vector3::UNIT_Z); + + mBase->setOrientation (xr*yr*zr); + + // scale + mBase->setScale (reference.mScale, reference.mScale, reference.mScale); +} + +const CSMWorld::CellRef& CSVRender::Object::getReference() const +{ + if (mReferenceId.empty()) + throw std::logic_error ("object does not represent a reference"); + + return mData.getReferences().getRecord (mReferenceId).get(); +} + +CSVRender::Object::Object (const CSMWorld::Data& data, Ogre::SceneNode *cellNode, + const std::string& id, bool referenceable, CSVWorld::PhysicsSystem *physics, + bool forceBaseToZero) +: mData (data), mBase (0), mForceBaseToZero (forceBaseToZero), mPhysics(physics) +{ + mBase = cellNode->createChildSceneNode(); + + if (referenceable) + { + mReferenceableId = id; + } + else + { + mReferenceId = id; + mReferenceableId = getReference().mRefID; + } + + update(); + adjust(); +} + +CSVRender::Object::~Object() +{ + clear(); + + mPhysics->removeObject(mBase->getName()); + + if (mBase) + mBase->getCreator()->destroySceneNode (mBase); +} + +bool CSVRender::Object::referenceableDataChanged (const QModelIndex& topLeft, + const QModelIndex& bottomRight) +{ + const CSMWorld::RefIdCollection& referenceables = mData.getReferenceables(); + + int index = referenceables.searchId (mReferenceableId); + + if (index!=-1 && index>=topLeft.row() && index<=bottomRight.row()) + { + update(); + adjust(); + return true; + } + + return false; +} + +bool CSVRender::Object::referenceableAboutToBeRemoved (const QModelIndex& parent, int start, + int end) +{ + const CSMWorld::RefIdCollection& referenceables = mData.getReferenceables(); + + int index = referenceables.searchId (mReferenceableId); + + if (index!=-1 && index>=start && index<=end) + { + // Deletion of referenceable-type objects is handled outside of Object. + if (!mReferenceId.empty()) + { + update(); + adjust(); + return true; + } + } + + return false; +} + +bool CSVRender::Object::referenceDataChanged (const QModelIndex& topLeft, + const QModelIndex& bottomRight) +{ + if (mReferenceId.empty()) + return false; + + const CSMWorld::RefCollection& references = mData.getReferences(); + + int index = references.searchId (mReferenceId); + + if (index!=-1 && index>=topLeft.row() && index<=bottomRight.row()) + { + int columnIndex = + references.findColumnIndex (CSMWorld::Columns::ColumnId_ReferenceableId); + + if (columnIndex>=topLeft.column() && columnIndex<=bottomRight.row()) + { + mReferenceableId = + references.getData (index, columnIndex).toString().toUtf8().constData(); + + update(); + } + + adjust(); + + return true; + } + + return false; +} + +std::string CSVRender::Object::getReferenceId() const +{ + return mReferenceId; +} + +std::string CSVRender::Object::getReferenceableId() const +{ + return mReferenceableId; +} diff --git a/apps/opencs/view/render/object.hpp b/apps/opencs/view/render/object.hpp new file mode 100644 index 000000000..eba2dc814 --- /dev/null +++ b/apps/opencs/view/render/object.hpp @@ -0,0 +1,87 @@ +#ifndef OPENCS_VIEW_OBJECT_H +#define OPENCS_VIEW_OBJECT_H + +#include + +class QModelIndex; + +namespace Ogre +{ + class SceneNode; +} + +namespace CSMWorld +{ + class Data; + class CellRef; +} + +namespace CSVWorld +{ + class PhysicsSystem; +} + +namespace CSVRender +{ + class Object + { + const CSMWorld::Data& mData; + std::string mReferenceId; + std::string mReferenceableId; + Ogre::SceneNode *mBase; + NifOgre::ObjectScenePtr mObject; + bool mForceBaseToZero; + CSVWorld::PhysicsSystem *mPhysics; + + /// Not implemented + Object (const Object&); + + /// Not implemented + Object& operator= (const Object&); + + /// Destroy all scene nodes and movable objects attached to node. + static void clearSceneNode (Ogre::SceneNode *node); + + /// Remove object from node (includes deleting) + void clear(); + + /// Update model + void update(); + + /// Adjust position, orientation and scale + void adjust(); + + /// Throws an exception if *this was constructed with referenceable + const CSMWorld::CellRef& getReference() const; + + public: + + Object (const CSMWorld::Data& data, Ogre::SceneNode *cellNode, + const std::string& id, bool referenceable, + CSVWorld::PhysicsSystem *physics = NULL, bool forceBaseToZero = false); + /// \param forceBaseToZero If this is a reference ignore the coordinates and place + /// it at 0, 0, 0 instead. + + ~Object(); + + /// \return Did this call result in a modification of the visual representation of + /// this object? + bool referenceableDataChanged (const QModelIndex& topLeft, + const QModelIndex& bottomRight); + + /// \return Did this call result in a modification of the visual representation of + /// this object? + bool referenceableAboutToBeRemoved (const QModelIndex& parent, int start, int end); + + /// \return Did this call result in a modification of the visual representation of + /// this object? + bool referenceDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight); + + /// Returns an empty string if this is a refereceable-type object. + std::string getReferenceId() const; + + std::string getReferenceableId() const; + }; +} + +#endif diff --git a/apps/opencs/view/render/overlaymask.cpp b/apps/opencs/view/render/overlaymask.cpp new file mode 100644 index 000000000..09f020354 --- /dev/null +++ b/apps/opencs/view/render/overlaymask.cpp @@ -0,0 +1,52 @@ +#include "overlaymask.hpp" + +#include +#include + +#include "textoverlay.hpp" +#include "../../model/world/cellcoordinates.hpp" + +namespace CSVRender +{ + +// ideas from http://www.ogre3d.org/forums/viewtopic.php?f=5&t=44828#p486334 +OverlayMask::OverlayMask(std::map &overlays, Ogre::Viewport* viewport) + : mTextOverlays(overlays), mViewport(viewport) +{ +} + +OverlayMask::~OverlayMask() +{ +} + +void OverlayMask::setViewport(Ogre::Viewport *viewport) +{ + mViewport = viewport; +} + +void OverlayMask::preViewportUpdate(const Ogre::RenderTargetViewportEvent &event) +{ + if(event.source == mViewport) + { + Ogre::OverlayManager &overlayMgr = Ogre::OverlayManager::getSingleton(); + for(Ogre::OverlayManager::OverlayMapIterator iter = overlayMgr.getOverlayIterator(); + iter.hasMoreElements();) + { + Ogre::Overlay* item = iter.getNext(); + for(Ogre::Overlay::Overlay2DElementsIterator it = item->get2DElementsIterator(); + it.hasMoreElements();) + { + Ogre::OverlayContainer* container = it.getNext(); + if(container) container->hide(); + } + } + + std::map::iterator it = mTextOverlays.begin(); + for(; it != mTextOverlays.end(); ++it) + { + it->second->show(true); + } + } +} + +} diff --git a/apps/opencs/view/render/overlaymask.hpp b/apps/opencs/view/render/overlaymask.hpp new file mode 100644 index 000000000..ec050cac4 --- /dev/null +++ b/apps/opencs/view/render/overlaymask.hpp @@ -0,0 +1,42 @@ +#ifndef OPENCS_VIEW_OVERLAYMASK_H +#define OPENCS_VIEW_OVERLAYMASK_H + +#include + +namespace Ogre +{ + class Viewport; + class RendertargetViewportEvent; +} + +namespace CSMWorld +{ + class CellCoordinates; +} + +namespace CSVRender +{ + class TextOverlay; + + class OverlayMask : public Ogre::RenderTargetListener + { + + std::map &mTextOverlays; + Ogre::Viewport* mViewport; + + public: + + OverlayMask(std::map &overlays, + Ogre::Viewport* viewport); + + virtual ~OverlayMask(); + + void setViewport(Ogre::Viewport *viewport); + + protected: + + virtual void preViewportUpdate(const Ogre::RenderTargetViewportEvent &event); + }; +} + +#endif // OPENCS_VIEW_OVERLAYMASK_H diff --git a/apps/opencs/view/render/overlaysystem.cpp b/apps/opencs/view/render/overlaysystem.cpp new file mode 100644 index 000000000..f565f5af0 --- /dev/null +++ b/apps/opencs/view/render/overlaysystem.cpp @@ -0,0 +1,34 @@ +#include "overlaysystem.hpp" + +#include + +#include + +namespace CSVRender +{ + OverlaySystem *OverlaySystem::mOverlaySystemInstance = 0; + + OverlaySystem::OverlaySystem() + { + assert(!mOverlaySystemInstance); + mOverlaySystemInstance = this; + mOverlaySystem = new Ogre::OverlaySystem(); + } + + OverlaySystem::~OverlaySystem() + { + delete mOverlaySystem; + } + + OverlaySystem &OverlaySystem::instance() + { + assert(mOverlaySystemInstance); + return *mOverlaySystemInstance; + } + + Ogre::OverlaySystem *OverlaySystem::get() + { + return mOverlaySystem; + } +} + diff --git a/apps/opencs/view/render/overlaysystem.hpp b/apps/opencs/view/render/overlaysystem.hpp new file mode 100644 index 000000000..f8a78f329 --- /dev/null +++ b/apps/opencs/view/render/overlaysystem.hpp @@ -0,0 +1,26 @@ +#ifndef OPENCS_VIEW_OVERLAYSYSTEM_H +#define OPENCS_VIEW_OVERLAYSYSTEM_H + +namespace Ogre +{ + class OverlaySystem; +} + +namespace CSVRender +{ + class OverlaySystem + { + Ogre::OverlaySystem *mOverlaySystem; + static OverlaySystem *mOverlaySystemInstance; + + public: + + OverlaySystem(); + ~OverlaySystem(); + static OverlaySystem &instance(); + + Ogre::OverlaySystem *get(); + }; +} + +#endif // OPENCS_VIEW_OVERLAYSYSTEM_H diff --git a/apps/opencs/view/render/pagedworldspacewidget.cpp b/apps/opencs/view/render/pagedworldspacewidget.cpp index a3f34d218..e5d542858 100644 --- a/apps/opencs/view/render/pagedworldspacewidget.cpp +++ b/apps/opencs/view/render/pagedworldspacewidget.cpp @@ -3,13 +3,368 @@ #include -#include +#include -#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include "textoverlay.hpp" +#include "overlaymask.hpp" + +#include "../../model/world/tablemimedata.hpp" +#include "../../model/world/idtable.hpp" + +#include "../widget/scenetooltoggle.hpp" +#include "../widget/scenetoolmode.hpp" + +#include "editmode.hpp" +#include "elements.hpp" + +bool CSVRender::PagedWorldspaceWidget::adjustCells() +{ + bool modified = false; + bool setCamera = false; + + const CSMWorld::IdCollection& cells = mDocument.getData().getCells(); + + { + // remove (or name/region modified) + std::map::iterator iter (mCells.begin()); + + while (iter!=mCells.end()) + { + int index = cells.searchId (iter->first.getId (mWorldspace)); + + if (!mSelection.has (iter->first) || index==-1 || + cells.getRecord (index).mState==CSMWorld::RecordBase::State_Deleted) + { + // delete overlays + std::map::iterator itOverlay = mTextOverlays.find(iter->first); + if(itOverlay != mTextOverlays.end()) + { + delete itOverlay->second; + mTextOverlays.erase(itOverlay); + } + + // destroy manual objects + getSceneManager()->destroyManualObject("manual"+iter->first.getId(mWorldspace)); + + delete iter->second; + mCells.erase (iter++); + + modified = true; + } + else + { + // check if name or region field has changed + // FIXME: config setting + std::string name = cells.getRecord(index).get().mName; + std::string region = cells.getRecord(index).get().mRegion; + + std::map::iterator it = mTextOverlays.find(iter->first); + if(it != mTextOverlays.end()) + { + if(it->second->getDesc() != "") // previously had name + { + if(name != it->second->getDesc()) // new name + { + if(name != "") + it->second->setDesc(name); + else // name deleted, use region + it->second->setDesc(region); + it->second->update(); + } + } + else if(name != "") // name added + { + it->second->setDesc(name); + it->second->update(); + } + else if(region != it->second->getDesc()) // new region + { + it->second->setDesc(region); + it->second->update(); + } + modified = true; + } + ++iter; + } + } + } + + if (mCells.begin()==mCells.end()) + setCamera = true; + + // add + for (CSMWorld::CellSelection::Iterator iter (mSelection.begin()); iter!=mSelection.end(); + ++iter) + { + int index = cells.searchId (iter->getId (mWorldspace)); + + if (index > 0 && cells.getRecord (index).mState!=CSMWorld::RecordBase::State_Deleted && + mCells.find (*iter)==mCells.end()) + { + Cell *cell = new Cell (mDocument.getData(), getSceneManager(), + iter->getId (mWorldspace), getPhysics()); + mCells.insert (std::make_pair (*iter, cell)); + + float height = cell->getTerrainHeightAt(Ogre::Vector3( + ESM::Land::REAL_SIZE * iter->getX() + ESM::Land::REAL_SIZE/2, + ESM::Land::REAL_SIZE * iter->getY() + ESM::Land::REAL_SIZE/2, + 0)); + if (setCamera) + { + setCamera = false; + getCamera()->setPosition ( + ESM::Land::REAL_SIZE * iter->getX() + ESM::Land::REAL_SIZE/2, + ESM::Land::REAL_SIZE * iter->getY() + ESM::Land::REAL_SIZE/2, + height); + // better camera position at the start + getCamera()->move(getCamera()->getDirection() * -6000); // FIXME: config setting + } + + Ogre::ManualObject* manual = + getSceneManager()->createManualObject("manual" + iter->getId(mWorldspace)); + manual->begin("BaseWhite", Ogre::RenderOperation::OT_LINE_LIST); + // define start and end point (x, y, z) + manual-> position(ESM::Land::REAL_SIZE * iter->getX() + ESM::Land::REAL_SIZE/2, + ESM::Land::REAL_SIZE * iter->getY() + ESM::Land::REAL_SIZE/2, + height); + manual-> position(ESM::Land::REAL_SIZE * iter->getX() + ESM::Land::REAL_SIZE/2, + ESM::Land::REAL_SIZE * iter->getY() + ESM::Land::REAL_SIZE/2, + height+200); // FIXME: config setting + manual->end(); + manual->setBoundingBox(Ogre::AxisAlignedBox( + ESM::Land::REAL_SIZE * iter->getX() + ESM::Land::REAL_SIZE/2, + ESM::Land::REAL_SIZE * iter->getY() + ESM::Land::REAL_SIZE/2, + height, + ESM::Land::REAL_SIZE * iter->getX() + ESM::Land::REAL_SIZE/2, + ESM::Land::REAL_SIZE * iter->getY() + ESM::Land::REAL_SIZE/2, + height+200)); + getSceneManager()->getRootSceneNode()->createChildSceneNode()->attachObject(manual); + manual->setVisible(false); + + CSVRender::TextOverlay *textDisp = + new CSVRender::TextOverlay(manual, getCamera(), iter->getId(mWorldspace)); + textDisp->enable(true); + textDisp->setCaption(iter->getId(mWorldspace)); + std::string desc = cells.getRecord(index).get().mName; + if(desc == "") desc = cells.getRecord(index).get().mRegion; + textDisp->setDesc(desc); // FIXME: config setting + textDisp->update(); + mTextOverlays.insert(std::make_pair(*iter, textDisp)); + if(!mOverlayMask) + { + mOverlayMask = new OverlayMask(mTextOverlays, getViewport()); + addRenderTargetListener(mOverlayMask); + } + + modified = true; + } + } + + return modified; +} + +void CSVRender::PagedWorldspaceWidget::mousePressEvent (QMouseEvent *event) +{ + if(event->button() == Qt::RightButton) + { + std::map::iterator iter = mTextOverlays.begin(); + for(; iter != mTextOverlays.end(); ++iter) + { + if(mDisplayCellCoord && + iter->second->isEnabled() && iter->second->container().contains(event->x(), event->y())) + { + return; + } + } + } + WorldspaceWidget::mousePressEvent(event); +} + +void CSVRender::PagedWorldspaceWidget::mouseReleaseEvent (QMouseEvent *event) +{ + if(event->button() == Qt::RightButton) + { + std::map::iterator iter = mTextOverlays.begin(); + for(; iter != mTextOverlays.end(); ++iter) + { + if(mDisplayCellCoord && + iter->second->isEnabled() && iter->second->container().contains(event->x(), event->y())) + { + std::cout << "clicked: " << iter->second->getCaption() << std::endl; + return; + } + } + } + WorldspaceWidget::mouseReleaseEvent(event); +} + +void CSVRender::PagedWorldspaceWidget::mouseDoubleClickEvent (QMouseEvent *event) +{ + WorldspaceWidget::mouseDoubleClickEvent(event); +} + +void CSVRender::PagedWorldspaceWidget::addEditModeSelectorButtons ( + CSVWidget::SceneToolMode *tool) +{ + WorldspaceWidget::addEditModeSelectorButtons (tool); + + /// \todo replace EditMode with suitable subclasses + tool->addButton ( + new EditMode (this, QIcon (":placeholder"), Element_Reference, "Terrain shape editing"), + "terrain-shape"); + tool->addButton ( + new EditMode (this, QIcon (":placeholder"), Element_Reference, "Terrain texture editing"), + "terrain-texture"); + tool->addButton ( + new EditMode (this, QIcon (":placeholder"), Element_Reference, "Terrain vertex paint editing"), + "terrain-vertex"); + tool->addButton ( + new EditMode (this, QIcon (":placeholder"), Element_Reference, "Terrain movement"), + "terrain-move"); +} + +void CSVRender::PagedWorldspaceWidget::updateOverlay() +{ + if(getCamera()->getViewport()) + { + if((uint32_t)getCamera()->getViewport()->getVisibilityMask() + & (uint32_t)CSVRender::Element_CellMarker) + mDisplayCellCoord = true; + else + mDisplayCellCoord = false; + } + + if(!mTextOverlays.empty()) + { + std::map::iterator it = mTextOverlays.begin(); + for(; it != mTextOverlays.end(); ++it) + { + it->second->enable(mDisplayCellCoord); + it->second->update(); + } + } +} + +void CSVRender::PagedWorldspaceWidget::referenceableDataChanged (const QModelIndex& topLeft, + const QModelIndex& bottomRight) +{ + for (std::map::iterator iter (mCells.begin()); + iter!=mCells.end(); ++iter) + if (iter->second->referenceableDataChanged (topLeft, bottomRight)) + flagAsModified(); +} + +void CSVRender::PagedWorldspaceWidget::referenceableAboutToBeRemoved ( + const QModelIndex& parent, int start, int end) +{ + for (std::map::iterator iter (mCells.begin()); + iter!=mCells.end(); ++iter) + if (iter->second->referenceableAboutToBeRemoved (parent, start, end)) + flagAsModified(); +} + +void CSVRender::PagedWorldspaceWidget::referenceableAdded (const QModelIndex& parent, + int start, int end) +{ + CSMWorld::IdTable& referenceables = dynamic_cast ( + *mDocument.getData().getTableModel (CSMWorld::UniversalId::Type_Referenceables)); + + for (std::map::iterator iter (mCells.begin()); + iter!=mCells.end(); ++iter) + { + QModelIndex topLeft = referenceables.index (start, 0); + QModelIndex bottomRight = + referenceables.index (end, referenceables.columnCount()); + + if (iter->second->referenceableDataChanged (topLeft, bottomRight)) + flagAsModified(); + } +} + +void CSVRender::PagedWorldspaceWidget::referenceDataChanged (const QModelIndex& topLeft, + const QModelIndex& bottomRight) +{ + for (std::map::iterator iter (mCells.begin()); + iter!=mCells.end(); ++iter) + if (iter->second->referenceDataChanged (topLeft, bottomRight)) + flagAsModified(); +} + +void CSVRender::PagedWorldspaceWidget::referenceAboutToBeRemoved (const QModelIndex& parent, + int start, int end) +{ + for (std::map::iterator iter (mCells.begin()); + iter!=mCells.end(); ++iter) + if (iter->second->referenceAboutToBeRemoved (parent, start, end)) + flagAsModified(); +} + +void CSVRender::PagedWorldspaceWidget::referenceAdded (const QModelIndex& parent, int start, + int end) +{ + for (std::map::iterator iter (mCells.begin()); + iter!=mCells.end(); ++iter) + if (iter->second->referenceAdded (parent, start, end)) + flagAsModified(); +} + +std::string CSVRender::PagedWorldspaceWidget::getStartupInstruction() +{ + Ogre::Vector3 position = getCamera()->getPosition(); + + std::ostringstream stream; + + stream + << "player->position " + << position.x << ", " << position.y << ", " << position.z + << ", 0"; + + return stream.str(); +} CSVRender::PagedWorldspaceWidget::PagedWorldspaceWidget (QWidget* parent, CSMDoc::Document& document) -: WorldspaceWidget (document, parent) -{} +: WorldspaceWidget (document, parent), mDocument (document), mWorldspace ("std::default"), + mControlElements(NULL), mDisplayCellCoord(true), mOverlayMask(NULL) +{ + QAbstractItemModel *cells = + document.getData().getTableModel (CSMWorld::UniversalId::Type_Cells); + + connect (cells, SIGNAL (dataChanged (const QModelIndex&, const QModelIndex&)), + this, SLOT (cellDataChanged (const QModelIndex&, const QModelIndex&))); + connect (cells, SIGNAL (rowsRemoved (const QModelIndex&, int, int)), + this, SLOT (cellRemoved (const QModelIndex&, int, int))); + connect (cells, SIGNAL (rowsInserted (const QModelIndex&, int, int)), + this, SLOT (cellAdded (const QModelIndex&, int, int))); +} + +CSVRender::PagedWorldspaceWidget::~PagedWorldspaceWidget() +{ + for (std::map::iterator iter (mCells.begin()); + iter!=mCells.end(); ++iter) + { + delete iter->second; + + getSceneManager()->destroyManualObject("manual"+iter->first.getId(mWorldspace)); + } + + for (std::map::iterator iter (mTextOverlays.begin()); + iter != mTextOverlays.end(); ++iter) + { + delete iter->second; + } + + removeRenderTargetListener(mOverlayMask); + delete mOverlayMask; +} void CSVRender::PagedWorldspaceWidget::useViewHint (const std::string& hint) { @@ -47,6 +402,10 @@ void CSVRender::PagedWorldspaceWidget::useViewHint (const std::string& hint) void CSVRender::PagedWorldspaceWidget::setCellSelection (const CSMWorld::CellSelection& selection) { mSelection = selection; + + if (adjustCells()) + flagAsModified(); + emit cellSelectionChanged (mSelection); } @@ -59,8 +418,15 @@ std::pair< int, int > CSVRender::PagedWorldspaceWidget::getCoordinatesFromId (co return std::make_pair(x, y); } -void CSVRender::PagedWorldspaceWidget::handleDrop (const std::vector< CSMWorld::UniversalId >& data) +bool CSVRender::PagedWorldspaceWidget::handleDrop ( + const std::vector< CSMWorld::UniversalId >& data, DropType type) { + if (WorldspaceWidget::handleDrop (data, type)) + return true; + + if (type!=Type_CellsExterior) + return false; + bool selectionChanged = false; for (unsigned i = 0; i < data.size(); ++i) { @@ -72,21 +438,78 @@ void CSVRender::PagedWorldspaceWidget::handleDrop (const std::vector< CSMWorld:: } if (selectionChanged) { + if (adjustCells()) + flagAsModified(); + emit cellSelectionChanged(mSelection); } + + return true; } -CSVRender::WorldspaceWidget::dropRequirments CSVRender::PagedWorldspaceWidget::getDropRequirements (CSVRender::WorldspaceWidget::dropType type) const +CSVRender::WorldspaceWidget::dropRequirments CSVRender::PagedWorldspaceWidget::getDropRequirements (CSVRender::WorldspaceWidget::DropType type) const { + dropRequirments requirements = WorldspaceWidget::getDropRequirements (type); + + if (requirements!=ignored) + return requirements; + switch (type) { - case cellsExterior: + case Type_CellsExterior: return canHandle; - case cellsInterior: + case Type_CellsInterior: return needUnpaged; default: return ignored; } -} \ No newline at end of file +} + +unsigned int CSVRender::PagedWorldspaceWidget::getVisibilityMask() const +{ + return WorldspaceWidget::getVisibilityMask() | mControlElements->getSelection(); +} + +CSVWidget::SceneToolToggle *CSVRender::PagedWorldspaceWidget::makeControlVisibilitySelector ( + CSVWidget::SceneToolbar *parent) +{ + mControlElements = new CSVWidget::SceneToolToggle (parent, + "Controls & Guides Visibility", ":placeholder"); + + mControlElements->addButton (":placeholder", Element_CellMarker, ":placeholder", + "Cell marker"); + mControlElements->addButton (":placeholder", Element_CellArrow, ":placeholder", "Cell arrows"); + mControlElements->addButton (":placeholder", Element_CellBorder, ":placeholder", "Cell border"); + + mControlElements->setSelection (0xffffffff); + + connect (mControlElements, SIGNAL (selectionChanged()), + this, SLOT (elementSelectionChanged())); + + return mControlElements; +} + +void CSVRender::PagedWorldspaceWidget::cellDataChanged (const QModelIndex& topLeft, + const QModelIndex& bottomRight) +{ + /// \todo check if no selected cell is affected and do not update, if that is the case + if (adjustCells()) + flagAsModified(); +} + +void CSVRender::PagedWorldspaceWidget::cellRemoved (const QModelIndex& parent, int start, + int end) +{ + if (adjustCells()) + flagAsModified(); +} + +void CSVRender::PagedWorldspaceWidget::cellAdded (const QModelIndex& index, int start, + int end) +{ + /// \todo check if no selected cell is affected and do not update, if that is the case + if (adjustCells()) + flagAsModified(); +} diff --git a/apps/opencs/view/render/pagedworldspacewidget.hpp b/apps/opencs/view/render/pagedworldspacewidget.hpp index 0a73c791c..59540a71e 100644 --- a/apps/opencs/view/render/pagedworldspacewidget.hpp +++ b/apps/opencs/view/render/pagedworldspacewidget.hpp @@ -1,22 +1,56 @@ #ifndef OPENCS_VIEW_PAGEDWORLDSPACEWIDGET_H #define OPENCS_VIEW_PAGEDWORLDSPACEWIDGET_H +#include + #include "../../model/world/cellselection.hpp" #include "worldspacewidget.hpp" +#include "cell.hpp" namespace CSVRender { + + class TextOverlay; + class OverlayMask; + class PagedWorldspaceWidget : public WorldspaceWidget { Q_OBJECT + CSMDoc::Document& mDocument; CSMWorld::CellSelection mSelection; + std::map mCells; + std::string mWorldspace; + CSVWidget::SceneToolToggle *mControlElements; + bool mDisplayCellCoord; + std::map mTextOverlays; + OverlayMask *mOverlayMask; private: std::pair getCoordinatesFromId(const std::string& record) const; + /// Bring mCells into sync with mSelection again. + /// + /// \return Any cells added or removed? + bool adjustCells(); + + virtual void referenceableDataChanged (const QModelIndex& topLeft, + const QModelIndex& bottomRight); + + virtual void referenceableAboutToBeRemoved (const QModelIndex& parent, int start, int end); + + virtual void referenceableAdded (const QModelIndex& index, int start, int end); + + virtual void referenceDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight); + + virtual void referenceAboutToBeRemoved (const QModelIndex& parent, int start, int end); + + virtual void referenceAdded (const QModelIndex& index, int start, int end); + + virtual std::string getStartupInstruction(); + public: PagedWorldspaceWidget (QWidget *parent, CSMDoc::Document& document); @@ -24,17 +58,49 @@ namespace CSVRender /// no cells are displayed. The cells to be displayed will be specified later through /// hint system. + virtual ~PagedWorldspaceWidget(); + void useViewHint (const std::string& hint); - void setCellSelection (const CSMWorld::CellSelection& selection); + void setCellSelection(const CSMWorld::CellSelection& selection); + + /// \return Drop handled? + virtual bool handleDrop (const std::vector& data, + DropType type); - virtual void handleDrop(const std::vector& data); + virtual dropRequirments getDropRequirements(DropType type) const; - virtual dropRequirments getDropRequirements(dropType type) const; + /// \attention The created tool is not added to the toolbar (via addTool). Doing + /// that is the responsibility of the calling function. + virtual CSVWidget::SceneToolToggle *makeControlVisibilitySelector ( + CSVWidget::SceneToolbar *parent); + + virtual unsigned int getVisibilityMask() const; + + protected: + + virtual void addEditModeSelectorButtons (CSVWidget::SceneToolMode *tool); + + virtual void updateOverlay(); + + virtual void mousePressEvent (QMouseEvent *event); + + virtual void mouseReleaseEvent (QMouseEvent *event); + + virtual void mouseDoubleClickEvent (QMouseEvent *event); signals: void cellSelectionChanged (const CSMWorld::CellSelection& selection); + + private slots: + + virtual void cellDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight); + + virtual void cellRemoved (const QModelIndex& parent, int start, int end); + + virtual void cellAdded (const QModelIndex& index, int start, int end); + }; } diff --git a/apps/opencs/view/render/previewwidget.cpp b/apps/opencs/view/render/previewwidget.cpp index 99e57ea11..f972c6361 100644 --- a/apps/opencs/view/render/previewwidget.cpp +++ b/apps/opencs/view/render/previewwidget.cpp @@ -7,194 +7,119 @@ #include "../../model/world/data.hpp" #include "../../model/world/idtable.hpp" -void CSVRender::PreviewWidget::setup() +CSVRender::PreviewWidget::PreviewWidget (CSMWorld::Data& data, + const std::string& id, bool referenceable, QWidget *parent) +: SceneWidget (parent), mData (data), + mObject (data, getSceneManager()->getRootSceneNode(), id, referenceable, NULL, true) { setNavigation (&mOrbit); - mNode = getSceneManager()->getRootSceneNode()->createChildSceneNode(); - mNode->setPosition (Ogre::Vector3 (0, 0, 0)); - - setModel(); - QAbstractItemModel *referenceables = mData.getTableModel (CSMWorld::UniversalId::Type_Referenceables); connect (referenceables, SIGNAL (dataChanged (const QModelIndex&, const QModelIndex&)), - this, SLOT (ReferenceableDataChanged (const QModelIndex&, const QModelIndex&))); + this, SLOT (referenceableDataChanged (const QModelIndex&, const QModelIndex&))); connect (referenceables, SIGNAL (rowsAboutToBeRemoved (const QModelIndex&, int, int)), - this, SLOT (ReferenceableAboutToBeRemoved (const QModelIndex&, int, int))); -} + this, SLOT (referenceableAboutToBeRemoved (const QModelIndex&, int, int))); -void CSVRender::PreviewWidget::setModel() -{ - if (mNode) + if (!referenceable) { - mObject.setNull(); - - if (mReferenceableId.empty()) - return; - - int column = - mData.getReferenceables().findColumnIndex (CSMWorld::Columns::ColumnId_Model); - - int row = mData.getReferenceables().searchId (mReferenceableId); + QAbstractItemModel *references = + mData.getTableModel (CSMWorld::UniversalId::Type_References); - if (row==-1) - return; - - QVariant value = mData.getReferenceables().getData (row, column); - - if (!value.isValid()) - return; - - std::string model = value.toString().toUtf8().constData(); - - if (model.empty()) - return; - - mObject = NifOgre::Loader::createObjects (mNode, "Meshes\\" + model); + connect (references, SIGNAL (dataChanged (const QModelIndex&, const QModelIndex&)), + this, SLOT (referenceDataChanged (const QModelIndex&, const QModelIndex&))); + connect (references, SIGNAL (rowsAboutToBeRemoved (const QModelIndex&, int, int)), + this, SLOT (referenceAboutToBeRemoved (const QModelIndex&, int, int))); } } -void CSVRender::PreviewWidget::adjust() -{ - if (mNode) - { - int row = mData.getReferences().getIndex (mReferenceId); - - float scale = mData.getReferences().getData (row, mData.getReferences(). - findColumnIndex (CSMWorld::Columns::ColumnId_Scale)).toFloat(); - float rotX = mData.getReferences().getData (row, mData.getReferences(). - findColumnIndex (CSMWorld::Columns::ColumnId_PositionXRot)).toFloat(); - float rotY = mData.getReferences().getData (row, mData.getReferences(). - findColumnIndex (CSMWorld::Columns::ColumnId_PositionYRot)).toFloat(); - float rotZ = mData.getReferences().getData (row, mData.getReferences(). - findColumnIndex (CSMWorld::Columns::ColumnId_PositionZRot)).toFloat(); - - mNode->setScale (scale, scale, scale); - - Ogre::Quaternion xr (Ogre::Radian(-rotX), Ogre::Vector3::UNIT_X); - - Ogre::Quaternion yr (Ogre::Radian(-rotY), Ogre::Vector3::UNIT_Y); - - Ogre::Quaternion zr (Ogre::Radian(-rotZ), Ogre::Vector3::UNIT_Z); - - mNode->setOrientation (xr*yr*zr); - } -} - -CSVRender::PreviewWidget::PreviewWidget (CSMWorld::Data& data, - const std::string& referenceableId, QWidget *parent) -: SceneWidget (parent), mData (data), mNode (0), mReferenceableId (referenceableId) -{ - setup(); -} - -CSVRender::PreviewWidget::PreviewWidget (CSMWorld::Data& data, - const std::string& referenceableId, const std::string& referenceId, QWidget *parent) -: SceneWidget (parent), mData (data), mReferenceableId (referenceableId), - mReferenceId (referenceId) -{ - setup(); - - adjust(); - - QAbstractItemModel *references = - mData.getTableModel (CSMWorld::UniversalId::Type_References); - - connect (references, SIGNAL (dataChanged (const QModelIndex&, const QModelIndex&)), - this, SLOT (ReferenceDataChanged (const QModelIndex&, const QModelIndex&))); - connect (references, SIGNAL (rowsAboutToBeRemoved (const QModelIndex&, int, int)), - this, SLOT (ReferenceAboutToBeRemoved (const QModelIndex&, int, int))); -} - -void CSVRender::PreviewWidget::ReferenceableDataChanged (const QModelIndex& topLeft, +void CSVRender::PreviewWidget::referenceableDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) { - if (mReferenceableId.empty()) - return; + if (mObject.referenceableDataChanged (topLeft, bottomRight)) + flagAsModified(); - CSMWorld::IdTable& referenceables = dynamic_cast ( - *mData.getTableModel (CSMWorld::UniversalId::Type_Referenceables)); + if (mObject.getReferenceId().empty()) + { + CSMWorld::IdTable& referenceables = dynamic_cast ( + *mData.getTableModel (CSMWorld::UniversalId::Type_Referenceables)); - QModelIndex index = referenceables.getModelIndex (mReferenceableId, 0); + QModelIndex index = referenceables.getModelIndex (mObject.getReferenceableId(), + referenceables.findColumnIndex (CSMWorld::Columns::ColumnId_Modification)); - if (index.row()>=topLeft.row() && index.row()<=bottomRight.row()) - { - /// \todo possible optimisation; check columns and only update if relevant columns have - /// changed - setModel(); - flagAsModified(); + if (referenceables.data (index).toInt()==CSMWorld::RecordBase::State_Deleted) + emit closeRequest(); } } -void CSVRender::PreviewWidget::ReferenceableAboutToBeRemoved (const QModelIndex& parent, int start, +void CSVRender::PreviewWidget::referenceableAboutToBeRemoved (const QModelIndex& parent, int start, int end) { - if (mReferenceableId.empty()) + if (mObject.referenceableAboutToBeRemoved (parent, start, end)) + flagAsModified(); + + if (mObject.getReferenceableId().empty()) return; CSMWorld::IdTable& referenceables = dynamic_cast ( *mData.getTableModel (CSMWorld::UniversalId::Type_Referenceables)); - QModelIndex index = referenceables.getModelIndex (mReferenceableId, 0); + QModelIndex index = referenceables.getModelIndex (mObject.getReferenceableId(), 0); if (index.row()>=start && index.row()<=end) { - if (mReferenceId.empty()) + if (mObject.getReferenceId().empty()) { // this is a preview for a referenceble emit closeRequest(); } - else - { - // this is a preview for a reference - mObject.setNull(); - flagAsModified(); - } } } -void CSVRender::PreviewWidget::ReferenceDataChanged (const QModelIndex& topLeft, +void CSVRender::PreviewWidget::referenceDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) { - if (mReferenceId.empty()) + if (mObject.referenceDataChanged (topLeft, bottomRight)) + flagAsModified(); + + if (mObject.getReferenceId().empty()) return; CSMWorld::IdTable& references = dynamic_cast ( *mData.getTableModel (CSMWorld::UniversalId::Type_References)); - int columnIndex = references.findColumnIndex (CSMWorld::Columns::ColumnId_ReferenceableId); - - QModelIndex index = references.getModelIndex (mReferenceId, columnIndex); - - if (index.row()>=topLeft.row() && index.row()<=bottomRight.row()) + // check for deleted state { - /// \todo possible optimisation; check columns and only update if relevant columns have - /// changed - adjust(); + QModelIndex index = references.getModelIndex (mObject.getReferenceId(), + references.findColumnIndex (CSMWorld::Columns::ColumnId_Modification)); - if (index.column()>=topLeft.column() && index.column()<=bottomRight.row()) + if (references.data (index).toInt()==CSMWorld::RecordBase::State_Deleted) { - mReferenceableId = references.data (index).toString().toUtf8().constData(); - emit referenceableIdChanged (mReferenceableId); - setModel(); + emit closeRequest(); + return; } - - flagAsModified(); } + + int columnIndex = references.findColumnIndex (CSMWorld::Columns::ColumnId_ReferenceableId); + + QModelIndex index = references.getModelIndex (mObject.getReferenceId(), columnIndex); + + if (index.row()>=topLeft.row() && index.row()<=bottomRight.row()) + if (index.column()>=topLeft.column() && index.column()<=bottomRight.row()) + emit referenceableIdChanged (mObject.getReferenceableId()); } -void CSVRender::PreviewWidget::ReferenceAboutToBeRemoved (const QModelIndex& parent, int start, +void CSVRender::PreviewWidget::referenceAboutToBeRemoved (const QModelIndex& parent, int start, int end) { - if (mReferenceId.empty()) + if (mObject.getReferenceId().empty()) return; CSMWorld::IdTable& references = dynamic_cast ( *mData.getTableModel (CSMWorld::UniversalId::Type_References)); - QModelIndex index = references.getModelIndex (mReferenceId, 0); + QModelIndex index = references.getModelIndex (mObject.getReferenceId(), 0); if (index.row()>=start && index.row()<=end) emit closeRequest(); diff --git a/apps/opencs/view/render/previewwidget.hpp b/apps/opencs/view/render/previewwidget.hpp index 7a63d8fb1..dd6a99c0f 100644 --- a/apps/opencs/view/render/previewwidget.hpp +++ b/apps/opencs/view/render/previewwidget.hpp @@ -1,11 +1,10 @@ #ifndef OPENCS_VIEW_PREVIEWWIDGET_H #define OPENCS_VIEW_PREVIEWWIDGET_H -#include - #include "scenewidget.hpp" #include "navigationorbit.hpp" +#include "object.hpp" class QModelIndex; @@ -22,26 +21,13 @@ namespace CSVRender CSMWorld::Data& mData; CSVRender::NavigationOrbit mOrbit; - NifOgre::ObjectScenePtr mObject; - Ogre::SceneNode *mNode; - std::string mReferenceId; - std::string mReferenceableId; - - void setup(); - - void setModel(); - - void adjust(); - ///< Adjust referenceable preview according to the reference + Object mObject; public: - PreviewWidget (CSMWorld::Data& data, const std::string& referenceableId, + PreviewWidget (CSMWorld::Data& data, const std::string& id, bool referenceable, QWidget *parent = 0); - PreviewWidget (CSMWorld::Data& data, const std::string& referenceableId, - const std::string& referenceId, QWidget *parent = 0); - signals: void closeRequest(); @@ -50,14 +36,14 @@ namespace CSVRender private slots: - void ReferenceableDataChanged (const QModelIndex& topLeft, + void referenceableDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight); - void ReferenceableAboutToBeRemoved (const QModelIndex& parent, int start, int end); + void referenceableAboutToBeRemoved (const QModelIndex& parent, int start, int end); - void ReferenceDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight); + void referenceDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight); - void ReferenceAboutToBeRemoved (const QModelIndex& parent, int start, int end); + void referenceAboutToBeRemoved (const QModelIndex& parent, int start, int end); }; } diff --git a/apps/opencs/view/render/scenewidget.cpp b/apps/opencs/view/render/scenewidget.cpp index 52e7afefd..55cf039fc 100644 --- a/apps/opencs/view/render/scenewidget.cpp +++ b/apps/opencs/view/render/scenewidget.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -10,23 +11,28 @@ #include #include #include +#include -#include "../world/scenetoolmode.hpp" +#include "../widget/scenetoolmode.hpp" +#include "../../model/settings/usersettings.hpp" #include "navigation.hpp" #include "lighting.hpp" +#include "overlaysystem.hpp" namespace CSVRender { SceneWidget::SceneWidget(QWidget *parent) : QWidget(parent) - , mWindow(NULL) , mCamera(NULL) - , mSceneMgr(NULL), mNavigation (0), mLighting (0), mUpdate (false) - , mKeyForward (false), mKeyBackward (false), mKeyLeft (false), mKeyRight (false) + , mSceneMgr(NULL) + , mWindow(NULL) + , mViewport(NULL) + , mNavigation (0), mLighting (0), mUpdate (false), mKeyForward (false) + , mKeyBackward (false), mKeyLeft (false), mKeyRight (false) , mKeyRollLeft (false), mKeyRollRight (false) , mFast (false), mDragging (false), mMod1 (false) - , mFastFactor (4) /// \todo make this configurable + , mFastFactor (4) , mDefaultAmbient (0, 0, 0, 0), mHasDefaultAmbient (false) { setAttribute(Qt::WA_PaintOnScreen); @@ -43,24 +49,54 @@ namespace CSVRender mCamera->setPosition (300, 0, 0); mCamera->lookAt (0, 0, 0); mCamera->setNearClipDistance (0.1); - mCamera->setFarClipDistance (30000); + + CSMSettings::UserSettings &userSettings = CSMSettings::UserSettings::instance(); + + float farClipDist = userSettings.setting("3d-render/far-clip-distance", QString("300000")).toFloat(); + mCamera->setFarClipDistance (farClipDist); + + mFastFactor = userSettings.setting("scene-input/fast-factor", QString("4")).toInt(); + mCamera->roll (Ogre::Degree (90)); setLighting (&mLightingDay); + mOverlaySystem = OverlaySystem::instance().get(); + mSceneMgr->addRenderQueueListener(mOverlaySystem); + QTimer *timer = new QTimer (this); connect (timer, SIGNAL (timeout()), this, SLOT (update())); - timer->start (20); /// \todo make this configurable + + int timerStart = userSettings.setting("scene-input/timer", QString("20")).toInt(); + timer->start (timerStart); + + /// \todo make shortcut configurable + QShortcut *focusToolbar = new QShortcut (Qt::Key_T, this, 0, 0, Qt::WidgetWithChildrenShortcut); + connect (focusToolbar, SIGNAL (activated()), this, SIGNAL (focusToolbarRequest())); } - CSVWorld::SceneToolMode *SceneWidget::makeLightingSelector (CSVWorld::SceneToolbar *parent) + CSVWidget::SceneToolMode *SceneWidget::makeLightingSelector (CSVWidget::SceneToolbar *parent) { - CSVWorld::SceneToolMode *tool = new CSVWorld::SceneToolMode (parent); - - tool->addButton (":door.png", "day"); /// \todo replace icons - tool->addButton (":GMST.png", "night"); - tool->addButton (":Info.png", "bright"); + CSVWidget::SceneToolMode *tool = new CSVWidget::SceneToolMode (parent, "Lighting Mode"); + + /// \todo replace icons + tool->addButton (":scenetoolbar/day", "day", + "Day" + "

  • Cell specific ambient in interiors
  • " + "
  • Low ambient in exteriors
  • " + "
  • Strong directional light source/lir>" + "
  • This mode closely resembles day time in-game
"); + tool->addButton (":scenetoolbar/night", "night", + "Night" + "
  • Cell specific ambient in interiors
  • " + "
  • Low ambient in exteriors
  • " + "
  • Weak directional light source
  • " + "
  • This mode closely resembles night time in-game
"); + tool->addButton (":scenetoolbar/bright", "bright", + "Bright" + "
  • Maximum ambient
  • " + "
  • Strong directional light source
"); connect (tool, SIGNAL (modeChanged (const std::string&)), this, SLOT (selectLightingMode (const std::string&))); @@ -87,7 +123,7 @@ namespace CSVRender std::stringstream windowHandle; #ifdef WIN32 - windowHandle << Ogre::StringConverter::toString((unsigned long)(this->winId())); + windowHandle << Ogre::StringConverter::toString((uintptr_t)(this->winId())); #else windowHandle << this->winId(); #endif @@ -99,7 +135,16 @@ namespace CSVRender params.insert(std::make_pair("externalWindowHandle", windowHandle.str())); params.insert(std::make_pair("title", windowTitle.str())); - params.insert(std::make_pair("FSAA", "0")); // TODO setting + + std::string antialiasing = + CSMSettings::UserSettings::instance().settingValue("3d-render/antialiasing").toStdString(); + if(antialiasing == "MSAA 16") antialiasing = "16"; + else if(antialiasing == "MSAA 8") antialiasing = "8"; + else if(antialiasing == "MSAA 4") antialiasing = "4"; + else if(antialiasing == "MSAA 2") antialiasing = "2"; + else antialiasing = "0"; + params.insert(std::make_pair("FSAA", antialiasing)); + params.insert(std::make_pair("vsync", "false")); // TODO setting #if OGRE_PLATFORM == OGRE_PLATFORM_APPLE params.insert(std::make_pair("macAPI", "cocoa")); @@ -107,7 +152,9 @@ namespace CSVRender #endif mWindow = Ogre::Root::getSingleton().createRenderWindow(windowTitle.str(), this->width(), this->height(), false, ¶ms); - mWindow->addViewport(mCamera)->setBackgroundColour(Ogre::ColourValue(0.3,0.3,0.3,1)); + + mViewport = mWindow->addViewport (mCamera); + mViewport->setBackgroundColour (Ogre::ColourValue (0.3,0.3,0.3,1)); Ogre::Real aspectRatio = Ogre::Real(width()) / Ogre::Real(height()); mCamera->setAspectRatio(aspectRatio); @@ -118,8 +165,17 @@ namespace CSVRender if (mWindow) Ogre::Root::getSingleton().destroyRenderTarget (mWindow); + if (mSceneMgr) + mSceneMgr->removeRenderQueueListener (mOverlaySystem); + if (mSceneMgr) Ogre::Root::getSingleton().destroySceneManager (mSceneMgr); + + } + + void SceneWidget::setVisibilityMask (unsigned int mask) + { + mViewport->setVisibilityMask (mask); } void SceneWidget::setNavigation (Navigation *navigation) @@ -132,11 +188,34 @@ namespace CSVRender } } + void SceneWidget::addRenderTargetListener(Ogre::RenderTargetListener *listener) + { + mWindow->addListener(listener); + } + + void SceneWidget::removeRenderTargetListener(Ogre::RenderTargetListener *listener) + { + mWindow->removeListener(listener); + } + + Ogre::Viewport *SceneWidget::getViewport() + { + if (!mWindow) + updateOgreWindow(); + + return mViewport; + } + Ogre::SceneManager *SceneWidget::getSceneManager() { return mSceneMgr; } + Ogre::Camera *SceneWidget::getCamera() + { + return mCamera; + } + void SceneWidget::flagAsModified() { mUpdate = true; @@ -327,14 +406,18 @@ namespace CSVRender { mUpdate = false; mWindow->update(); + updateOverlay(); } } - int SceneWidget::getFastFactor() const + void SceneWidget::updateScene() { - return mFast ? mFastFactor : 1; + flagAsModified(); } + void SceneWidget::updateOverlay() + { } + void SceneWidget::setLighting (Lighting *lighting) { if (mLighting) @@ -342,6 +425,9 @@ namespace CSVRender mLighting = lighting; mLighting->activate (mSceneMgr, mHasDefaultAmbient ? &mDefaultAmbient : 0); + + if (mWindow) + mWindow->update(); } void SceneWidget::selectLightingMode (const std::string& mode) @@ -353,4 +439,32 @@ namespace CSVRender else if (mode=="bright") setLighting (&mLightingBright); } + + void SceneWidget::updateUserSetting (const QString &key, const QStringList &list) + { + if(key.contains(QRegExp("^\\b(Objects|Shader|Scene)", Qt::CaseInsensitive))) + flagAsModified(); + + if(key == "3d-render/far-clip-distance" && !list.empty()) + { + if(mCamera->getFarClipDistance() != list.at(0).toFloat()) + mCamera->setFarClipDistance(list.at(0).toFloat()); + } + + // minimise unnecessary ogre window creation by updating only when there is a change + if(key == "3d-render/antialiasing") + { + unsigned int aa = mWindow->getFSAA(); + unsigned int antialiasing = 0; + if(!list.empty()) + { + if(list.at(0) == "MSAA 16") antialiasing = 16; + else if(list.at(0) == "MSAA 8") antialiasing = 8; + else if(list.at(0) == "MSAA 4") antialiasing = 4; + else if(list.at(0) == "MSAA 2") antialiasing = 2; + } + if(aa != antialiasing) + updateOgreWindow(); + } + } } diff --git a/apps/opencs/view/render/scenewidget.hpp b/apps/opencs/view/render/scenewidget.hpp index 7f8f104f1..699d6a7a5 100644 --- a/apps/opencs/view/render/scenewidget.hpp +++ b/apps/opencs/view/render/scenewidget.hpp @@ -14,9 +14,12 @@ namespace Ogre class Camera; class SceneManager; class RenderWindow; + class Viewport; + class OverlaySystem; + class RenderTargetListener; } -namespace CSVWorld +namespace CSVWidget { class SceneToolMode; class SceneToolbar; @@ -38,51 +41,65 @@ namespace CSVRender QPaintEngine* paintEngine() const; - CSVWorld::SceneToolMode *makeLightingSelector (CSVWorld::SceneToolbar *parent); + CSVWidget::SceneToolMode *makeLightingSelector (CSVWidget::SceneToolbar *parent); ///< \attention The created tool is not added to the toolbar (via addTool). Doing that /// is the responsibility of the calling function. + virtual void setVisibilityMask (unsigned int mask); + + virtual void updateScene(); + protected: void setNavigation (Navigation *navigation); ///< \attention The ownership of \a navigation is not transferred to *this. + void addRenderTargetListener(Ogre::RenderTargetListener *listener); + + void removeRenderTargetListener(Ogre::RenderTargetListener *listener); + + Ogre::Viewport *getViewport(); + Ogre::SceneManager *getSceneManager(); + Ogre::Camera *getCamera(); + void flagAsModified(); void setDefaultAmbient (const Ogre::ColourValue& colour); ///< \note The actual ambient colour may differ based on lighting settings. + virtual void updateOverlay(); + + virtual void mouseReleaseEvent (QMouseEvent *event); + + virtual void mouseMoveEvent (QMouseEvent *event); + + void wheelEvent (QWheelEvent *event); + + void keyPressEvent (QKeyEvent *event); + private: void paintEvent(QPaintEvent* e); void resizeEvent(QResizeEvent* e); bool event(QEvent* e); - void keyPressEvent (QKeyEvent *event); - void keyReleaseEvent (QKeyEvent *event); void focusOutEvent (QFocusEvent *event); - void wheelEvent (QWheelEvent *event); - void leaveEvent (QEvent *event); - void mouseMoveEvent (QMouseEvent *event); - - void mouseReleaseEvent (QMouseEvent *event); - void updateOgreWindow(); - int getFastFactor() const; - void setLighting (Lighting *lighting); ///< \attention The ownership of \a lighting is not transferred to *this. - Ogre::Camera* mCamera; + Ogre::Camera* mCamera; Ogre::SceneManager* mSceneMgr; Ogre::RenderWindow* mWindow; + Ogre::Viewport *mViewport; + Ogre::OverlaySystem *mOverlaySystem; Navigation *mNavigation; Lighting *mLighting; @@ -104,11 +121,19 @@ namespace CSVRender LightingNight mLightingNight; LightingBright mLightingBright; + public slots: + + void updateUserSetting (const QString &key, const QStringList &list); + private slots: void update(); void selectLightingMode (const std::string& mode); + + signals: + + void focusToolbarRequest(); }; } diff --git a/apps/opencs/view/render/terrainstorage.cpp b/apps/opencs/view/render/terrainstorage.cpp new file mode 100644 index 000000000..a14eea5dd --- /dev/null +++ b/apps/opencs/view/render/terrainstorage.cpp @@ -0,0 +1,43 @@ +#include "terrainstorage.hpp" + +namespace CSVRender +{ + + TerrainStorage::TerrainStorage(const CSMWorld::Data &data) + : mData(data) + { + } + + ESM::Land* TerrainStorage::getLand(int cellX, int cellY) + { + std::ostringstream stream; + stream << "#" << cellX << " " << cellY; + + // The cell isn't guaranteed to have Land. This is because the terrain implementation + // has to wrap the vertices of the last row and column to the next cell, which may be a nonexisting cell + int index = mData.getLand().searchId(stream.str()); + if (index == -1) + return NULL; + + ESM::Land* land = mData.getLand().getRecord(index).get().mLand.get(); + int mask = ESM::Land::DATA_VHGT | ESM::Land::DATA_VNML | ESM::Land::DATA_VCLR | ESM::Land::DATA_VTEX; + if (!land->isDataLoaded(mask)) + land->loadData(mask); + return land; + } + + const ESM::LandTexture* TerrainStorage::getLandTexture(int index, short plugin) + { + std::ostringstream stream; + stream << index << "_" << plugin; + + return &mData.getLandTextures().getRecord(stream.str()).get(); + } + + void TerrainStorage::getBounds(float &minX, float &maxX, float &minY, float &maxY) + { + // not needed at the moment - this returns the bounds of the whole world, but we only edit individual cells + throw std::runtime_error("getBounds not implemented"); + } + +} diff --git a/apps/opencs/view/render/terrainstorage.hpp b/apps/opencs/view/render/terrainstorage.hpp new file mode 100644 index 000000000..97782ad17 --- /dev/null +++ b/apps/opencs/view/render/terrainstorage.hpp @@ -0,0 +1,29 @@ +#ifndef OPENCS_RENDER_TERRAINSTORAGE_H +#define OPENCS_RENDER_TERRAINSTORAGE_H + +#include + +#include "../../model/world/data.hpp" + +namespace CSVRender +{ + + /** + * @brief A bridge between the terrain component and OpenCS's terrain data storage. + */ + class TerrainStorage : public ESMTerrain::Storage + { + public: + TerrainStorage(const CSMWorld::Data& data); + private: + const CSMWorld::Data& mData; + + virtual ESM::Land* getLand (int cellX, int cellY); + virtual const ESM::LandTexture* getLandTexture(int index, short plugin); + + virtual void getBounds(float& minX, float& maxX, float& minY, float& maxY); + }; + +} + +#endif diff --git a/apps/opencs/view/render/textoverlay.cpp b/apps/opencs/view/render/textoverlay.cpp new file mode 100644 index 000000000..656ea959c --- /dev/null +++ b/apps/opencs/view/render/textoverlay.cpp @@ -0,0 +1,359 @@ +#include "textoverlay.hpp" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace CSVRender +{ + +// Things to do: +// - configurable font size in pixels (automatically calulate everything else from it) +// - configurable texture to use +// - try material script +// - decide whether to use QPaint (http://www.ogre3d.org/tikiwiki/Ogre+overlays+using+Qt) + +// http://www.ogre3d.org/tikiwiki/ObjectTextDisplay +// http://www.ogre3d.org/tikiwiki/MovableTextOverlay +// http://www.ogre3d.org/tikiwiki/Creating+dynamic+textures +// http://www.ogre3d.org/tikiwiki/ManualObject +TextOverlay::TextOverlay(const Ogre::MovableObject* obj, const Ogre::Camera* camera, const Ogre::String& id) + : mOverlay(0), mCaption(""), mDesc(""), mEnabled(true), mCamera(camera), mObj(obj), mId(id) + , mOnScreen(false) , mInstance(0), mFontHeight(16) // FIXME: make font height configurable +{ + if(id == "" || !camera || !obj) + throw std::runtime_error("TextOverlay could not be created."); + + // setup font + Ogre::FontManager &fontMgr = Ogre::FontManager::getSingleton(); + if (fontMgr.resourceExists("DejaVuLGC")) + mFont = fontMgr.getByName("DejaVuLGC","General"); + else + { + mFont = fontMgr.create("DejaVuLGC","General"); + mFont->setType(Ogre::FT_TRUETYPE); + mFont->setSource("DejaVuLGCSansMono.ttf"); + mFont->setTrueTypeSize(mFontHeight); + mFont->setTrueTypeResolution(96); + } + if(!mFont.isNull()) + mFont->load(); + else + throw std::runtime_error("TextOverlay font not loaded."); + + // setup overlay + Ogre::OverlayManager &overlayMgr = Ogre::OverlayManager::getSingleton(); + mOverlay = overlayMgr.getByName("CellIDPanel"+mId+Ogre::StringConverter::toString(mInstance)); + // FIXME: this logic is badly broken as it is possible to delete an earlier instance + while(mOverlay != NULL) + { + mInstance++; + mOverlay = overlayMgr.getByName("CellIDPanel"+mId+Ogre::StringConverter::toString(mInstance)); + } + mOverlay = overlayMgr.create("CellIDPanel"+mId+Ogre::StringConverter::toString(mInstance)); + + // create texture + Ogre::TexturePtr texture = Ogre::TextureManager::getSingleton().getByName("DynamicTransBlue"); + if(texture.isNull()) + { + texture = Ogre::TextureManager::getSingleton().createManual( + "DynamicTransBlue", // name + Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME, + Ogre::TEX_TYPE_2D, // type + 8, 8, // width & height + 0, // number of mipmaps + Ogre::PF_BYTE_BGRA, // pixel format + Ogre::TU_DEFAULT); // usage; should be TU_DYNAMIC_WRITE_ONLY_DISCARDABLE for + // textures updated very often (e.g. each frame) + + Ogre::HardwarePixelBufferSharedPtr pixelBuffer = texture->getBuffer(); + pixelBuffer->lock(Ogre::HardwareBuffer::HBL_NORMAL); + const Ogre::PixelBox& pixelBox = pixelBuffer->getCurrentLock(); + + Ogre::uint8* pDest = static_cast(pixelBox.data); + + // Fill in some pixel data. This will give a semi-transparent blue, + // but this is of course dependent on the chosen pixel format. + for (size_t j = 0; j < 8; j++) + { + for(size_t i = 0; i < 8; i++) + { + *pDest++ = 255; // B + *pDest++ = 0; // G + *pDest++ = 0; // R + *pDest++ = 63; // A + } + + pDest += pixelBox.getRowSkip() * Ogre::PixelUtil::getNumElemBytes(pixelBox.format); + } + pixelBuffer->unlock(); + } + + // setup material for containers + Ogre::MaterialPtr mQuadMaterial = Ogre::MaterialManager::getSingleton().getByName( + "TransOverlayMaterial"); + if(mQuadMaterial.isNull()) + { + Ogre::MaterialPtr mQuadMaterial = Ogre::MaterialManager::getSingleton().create( + "TransOverlayMaterial", + Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME, true ); + Ogre::Pass *pass = mQuadMaterial->getTechnique( 0 )->getPass( 0 ); + pass->setLightingEnabled( false ); + pass->setDepthWriteEnabled( false ); + pass->setSceneBlending( Ogre::SBT_TRANSPARENT_ALPHA ); + + Ogre::TextureUnitState *tex = pass->createTextureUnitState("MyCustomState", 0); + tex->setTextureName("DynamicTransBlue"); + tex->setTextureFiltering( Ogre::TFO_ANISOTROPIC ); + mQuadMaterial->load(); + } + + mContainer = static_cast(overlayMgr.createOverlayElement( + "Panel", "container"+mId +"#"+Ogre::StringConverter::toString(mInstance))); + mContainer->setMaterialName("TransOverlayMaterial"); + mOverlay->add2D(mContainer); + + // setup text area overlay element + mElement = static_cast(overlayMgr.createOverlayElement( + "TextArea", "text"+mId +"#"+Ogre::StringConverter::toString(mInstance))); + mElement->setMetricsMode(Ogre::GMM_RELATIVE); + mElement->setDimensions(1.0, 1.0); + mElement->setMetricsMode(Ogre::GMM_PIXELS); + mElement->setPosition(2*fontHeight()/3, 1.3*fontHeight()/3); // 1.3 & 2 = fudge factor + + mElement->setFontName("DejaVuLGC"); + mElement->setCharHeight(fontHeight()); // NOTE: seems that this is required as well as font->setTrueTypeSize() + mElement->setHorizontalAlignment(Ogre::GHA_LEFT); + //mElement->setColour(Ogre::ColourValue(1.0, 1.0, 1.0)); // R, G, B + mElement->setColour(Ogre::ColourValue(1.0, 1.0, 0)); // yellow + + mContainer->addChild(mElement); + mOverlay->show(); +} + +void TextOverlay::getScreenCoordinates(const Ogre::Vector3& position, Ogre::Real& x, Ogre::Real& y) +{ + Ogre::Vector3 hcsPosition = mCamera->getProjectionMatrix() * (mCamera->getViewMatrix() * position); + + x = 1.0f - ((hcsPosition.x * 0.5f) + 0.5f); // 0 <= x <= 1 // left := 0,right := 1 + y = ((hcsPosition.y * 0.5f) + 0.5f); // 0 <= y <= 1 // bottom := 0,top := 1 +} + +void TextOverlay::getMinMaxEdgesOfAABBIn2D(float& MinX, float& MinY, float& MaxX, float& MaxY, + bool top) +{ + MinX = 0, MinY = 0, MaxX = 0, MaxY = 0; + float X[4]; // the 2D dots of the AABB in screencoordinates + float Y[4]; + + if(!mObj->isInScene()) + return; + + const Ogre::AxisAlignedBox &AABB = mObj->getWorldBoundingBox(true); // the AABB of the target + Ogre::Vector3 cornersOfAABB[4]; + if(top) + { + cornersOfAABB[0] = AABB.getCorner(Ogre::AxisAlignedBox::FAR_LEFT_TOP); + cornersOfAABB[1] = AABB.getCorner(Ogre::AxisAlignedBox::FAR_RIGHT_TOP); + cornersOfAABB[2] = AABB.getCorner(Ogre::AxisAlignedBox::NEAR_LEFT_TOP); + cornersOfAABB[3] = AABB.getCorner(Ogre::AxisAlignedBox::NEAR_RIGHT_TOP); + } + else + { + cornersOfAABB[0] = AABB.getCorner(Ogre::AxisAlignedBox::FAR_LEFT_BOTTOM); + cornersOfAABB[1] = AABB.getCorner(Ogre::AxisAlignedBox::FAR_RIGHT_BOTTOM); + cornersOfAABB[2] = AABB.getCorner(Ogre::AxisAlignedBox::NEAR_LEFT_BOTTOM); + cornersOfAABB[3] = AABB.getCorner(Ogre::AxisAlignedBox::NEAR_RIGHT_BOTTOM); + } + + //The normal vector of the plane. This points directly infront of the camera. + Ogre::Vector3 cameraPlainNormal = mCamera->getDerivedOrientation().zAxis(); + + //the plane that devides the space before and behind the camera. + Ogre::Plane CameraPlane = Ogre::Plane(cameraPlainNormal, mCamera->getDerivedPosition()); + + for (int i = 0; i < 4; i++) + { + X[i] = 0; + Y[i] = 0; + + getScreenCoordinates(cornersOfAABB[i],X[i],Y[i]); // transfor into 2d dots + + if (CameraPlane.getSide(cornersOfAABB[i]) == Ogre::Plane::NEGATIVE_SIDE) + { + if (i == 0) // accept the first set of values, no matter how bad it might be. + { + MinX = X[i]; + MinY = Y[i]; + MaxX = X[i]; + MaxY = Y[i]; + } + else // now compare if you get "better" values + { + if (MinX > X[i]) MinX = X[i]; + if (MinY > Y[i]) MinY = Y[i]; + if (MaxX < X[i]) MaxX = X[i]; + if (MaxY < Y[i]) MaxY = Y[i]; + } + } + else + { + MinX = 0; + MinY = 0; + MaxX = 0; + MaxY = 0; + break; + } + } +} + +TextOverlay::~TextOverlay() +{ + Ogre::OverlayManager::OverlayMapIterator iter = Ogre::OverlayManager::getSingleton().getOverlayIterator(); + if(!iter.hasMoreElements()) + mOverlay->hide(); + + Ogre::OverlayManager *overlayMgr = Ogre::OverlayManager::getSingletonPtr(); + mContainer->removeChild("text"+mId+"#"+Ogre::StringConverter::toString(mInstance)); + mOverlay->remove2D(mContainer); + + if(!iter.hasMoreElements()) + overlayMgr->destroy(mOverlay); +} + +void TextOverlay::show(bool show) +{ + if(show && mOnScreen) + mContainer->show(); + else + mContainer->hide(); +} + +void TextOverlay::enable(bool enable) +{ + if(enable == mOverlay->isVisible()) + return; + + mEnabled = enable; + if(enable) + mOverlay->show(); + else + mOverlay->hide(); +} + +bool TextOverlay::isEnabled() +{ + return mEnabled; +} + +void TextOverlay::setCaption(const Ogre::String& text) +{ + if(mCaption == text) + return; + + mCaption = text; + mElement->setCaption(text); +} + +void TextOverlay::setDesc(const Ogre::String& text) +{ + if(mDesc == text) + return; + + mDesc = text; + mElement->setCaption(mCaption + ((text == "") ? "" : ("\n" + text))); +} + +Ogre::FontPtr TextOverlay::getFont() +{ + return mFont; +} + +int TextOverlay::textWidth() +{ + float captionWidth = 0; + float descWidth = 0; + + for(Ogre::String::const_iterator i = mCaption.begin(); i < mCaption.end(); ++i) + { + if(*i == 0x0020) + captionWidth += getFont()->getGlyphAspectRatio(0x0030); + else + captionWidth += getFont()->getGlyphAspectRatio(*i); + } + + for(Ogre::String::const_iterator i = mDesc.begin(); i < mDesc.end(); ++i) + { + if(*i == 0x0020) + descWidth += getFont()->getGlyphAspectRatio(0x0030); + else + descWidth += getFont()->getGlyphAspectRatio(*i); + } + + captionWidth *= fontHeight(); + descWidth *= fontHeight(); + + return (int) std::max(captionWidth, descWidth); +} + +int TextOverlay::fontHeight() +{ + return mFontHeight; +} + +void TextOverlay::update() +{ + float min_x, max_x, min_y, max_y; + getMinMaxEdgesOfAABBIn2D(min_x, min_y, max_x, max_y, false); + + if ((min_x>0.0) && (max_x<1.0) && (min_y>0.0) && (max_y<1.0)) + { + mOnScreen = true; + mContainer->show(); + } + else + { + mOnScreen = false; + mContainer->hide(); + return; + } + + getMinMaxEdgesOfAABBIn2D(min_x, min_y, max_x, max_y); + + Ogre::OverlayManager &overlayMgr = Ogre::OverlayManager::getSingleton(); + float viewportWidth = std::max(overlayMgr.getViewportWidth(), 1); // zero at the start + float viewportHeight = std::max(overlayMgr.getViewportHeight(), 1); // zero at the start + + int width = fontHeight()*2/3 + textWidth() + fontHeight()*2/3; // add margins + int height = fontHeight()/3 + fontHeight() + fontHeight()/3; + if(mDesc != "") + height = fontHeight()/3 + 2*fontHeight() + fontHeight()/3; + + float relTextWidth = width / viewportWidth; + float relTextHeight = height / viewportHeight; + + float posX = 1 - (min_x + max_x + relTextWidth)/2; + float posY = 1 - max_y - (relTextHeight-fontHeight()/3/viewportHeight); + + mContainer->setMetricsMode(Ogre::GMM_RELATIVE); + mContainer->setPosition(posX, posY); + mContainer->setDimensions(relTextWidth, relTextHeight); + + mPos = QRect(posX*viewportWidth, posY*viewportHeight, width, height); +} + +QRect TextOverlay::container() +{ + return mPos; +} + +} diff --git a/apps/opencs/view/render/textoverlay.hpp b/apps/opencs/view/render/textoverlay.hpp new file mode 100644 index 000000000..dbb347e56 --- /dev/null +++ b/apps/opencs/view/render/textoverlay.hpp @@ -0,0 +1,66 @@ +#ifndef OPENCS_VIEW_TEXTOVERLAY_H +#define OPENCS_VIEW_TEXTOVERLAY_H + +#include + +#include +#include + +namespace Ogre +{ + class MovableObject; + class Camera; + class Font; + class Overlay; + class OverlayContainer; + class TextAreaOverlayElement; +} + +namespace CSVRender +{ + + class TextOverlay + { + Ogre::Overlay* mOverlay; + Ogre::OverlayContainer* mContainer; + Ogre::TextAreaOverlayElement* mElement; + Ogre::String mCaption; + Ogre::String mDesc; + + const Ogre::MovableObject* mObj; + const Ogre::Camera* mCamera; + Ogre::FontPtr mFont; + int mFontHeight; // in pixels + Ogre::String mId; + QRect mPos; + + bool mEnabled; + bool mOnScreen; + int mInstance; + + Ogre::FontPtr getFont(); + int textWidth(); + int fontHeight(); + void getScreenCoordinates(const Ogre::Vector3& position, Ogre::Real& x, Ogre::Real& y); + void getMinMaxEdgesOfAABBIn2D(float& MinX, float& MinY, float& MaxX, float& MaxY, + bool top = true); + + public: + + TextOverlay(const Ogre::MovableObject* obj, const Ogre::Camera* camera, const Ogre::String &id); + virtual ~TextOverlay(); + + void enable(bool enable); // controlled from scene widget toolbar visibility mask + void show(bool show); // for updating from render target listener + bool isEnabled(); + void setCaption(const Ogre::String& text); + void setDesc(const Ogre::String& text); + void update(); + QRect container(); // for detection of mouse click on the overlay + Ogre::String getCaption() { return mCaption; } // FIXME: debug + Ogre::String getDesc() { return mDesc; } + }; + +} + +#endif // OPENCS_VIEW_TEXTOVERLAY_H diff --git a/apps/opencs/view/render/unpagedworldspacewidget.cpp b/apps/opencs/view/render/unpagedworldspacewidget.cpp index 166c85f44..8012b1b24 100644 --- a/apps/opencs/view/render/unpagedworldspacewidget.cpp +++ b/apps/opencs/view/render/unpagedworldspacewidget.cpp @@ -1,7 +1,10 @@ #include "unpagedworldspacewidget.hpp" +#include + #include +#include #include @@ -11,6 +14,10 @@ #include "../../model/world/idtable.hpp" #include "../../model/world/tablemimedata.hpp" +#include "../widget/scenetooltoggle.hpp" + +#include "elements.hpp" + void CSVRender::UnpagedWorldspaceWidget::update() { const CSMWorld::Record& record = @@ -21,6 +28,16 @@ void CSVRender::UnpagedWorldspaceWidget::update() setDefaultAmbient (colour); /// \todo deal with mSunlight and mFog/mForDensity + + flagAsModified(); +} + +void CSVRender::UnpagedWorldspaceWidget::addVisibilitySelectorButtons ( + CSVWidget::SceneToolToggle *tool) +{ + WorldspaceWidget::addVisibilitySelectorButtons (tool); + + tool->addButton (":armor.png", Element_Fog, ":armor.png", "Fog"); } CSVRender::UnpagedWorldspaceWidget::UnpagedWorldspaceWidget (const std::string& cellId, CSMDoc::Document& document, QWidget* parent) @@ -29,12 +46,17 @@ CSVRender::UnpagedWorldspaceWidget::UnpagedWorldspaceWidget (const std::string& mCellsModel = &dynamic_cast ( *document.getData().getTableModel (CSMWorld::UniversalId::Type_Cells)); + mReferenceablesModel = &dynamic_cast ( + *document.getData().getTableModel (CSMWorld::UniversalId::Type_Referenceables)); + connect (mCellsModel, SIGNAL (dataChanged (const QModelIndex&, const QModelIndex&)), this, SLOT (cellDataChanged (const QModelIndex&, const QModelIndex&))); connect (mCellsModel, SIGNAL (rowsAboutToBeRemoved (const QModelIndex&, int, int)), this, SLOT (cellRowsAboutToBeRemoved (const QModelIndex&, int, int))); update(); + + mCell.reset (new Cell (document.getData(), getSceneManager(), mCellId, getPhysics())); } void CSVRender::UnpagedWorldspaceWidget::cellDataChanged (const QModelIndex& topLeft, @@ -67,21 +89,104 @@ void CSVRender::UnpagedWorldspaceWidget::cellRowsAboutToBeRemoved (const QModelI emit closeRequest(); } -void CSVRender::UnpagedWorldspaceWidget::handleDrop (const std::vector< CSMWorld::UniversalId >& data) +bool CSVRender::UnpagedWorldspaceWidget::handleDrop (const std::vector& data, DropType type) { + if (WorldspaceWidget::handleDrop (data, type)) + return true; + + if (type!=Type_CellsInterior) + return false; + mCellId = data.begin()->getId(); + mCell.reset (new Cell (getDocument().getData(), getSceneManager(), mCellId, getPhysics())); + update(); emit cellChanged(*data.begin()); + + return true; +} + +void CSVRender::UnpagedWorldspaceWidget::referenceableDataChanged (const QModelIndex& topLeft, + const QModelIndex& bottomRight) +{ + if (mCell.get()) + if (mCell.get()->referenceableDataChanged (topLeft, bottomRight)) + flagAsModified(); } -CSVRender::WorldspaceWidget::dropRequirments CSVRender::UnpagedWorldspaceWidget::getDropRequirements (CSVRender::WorldspaceWidget::dropType type) const +void CSVRender::UnpagedWorldspaceWidget::referenceableAboutToBeRemoved ( + const QModelIndex& parent, int start, int end) { + if (mCell.get()) + if (mCell.get()->referenceableAboutToBeRemoved (parent, start, end)) + flagAsModified(); +} + +void CSVRender::UnpagedWorldspaceWidget::referenceableAdded (const QModelIndex& parent, + int start, int end) +{ + if (mCell.get()) + { + QModelIndex topLeft = mReferenceablesModel->index (start, 0); + QModelIndex bottomRight = + mReferenceablesModel->index (end, mReferenceablesModel->columnCount()); + + if (mCell.get()->referenceableDataChanged (topLeft, bottomRight)) + flagAsModified(); + } +} + +void CSVRender::UnpagedWorldspaceWidget::referenceDataChanged (const QModelIndex& topLeft, + const QModelIndex& bottomRight) +{ + if (mCell.get()) + if (mCell.get()->referenceDataChanged (topLeft, bottomRight)) + flagAsModified(); +} + +void CSVRender::UnpagedWorldspaceWidget::referenceAboutToBeRemoved (const QModelIndex& parent, + int start, int end) +{ + if (mCell.get()) + if (mCell.get()->referenceAboutToBeRemoved (parent, start, end)) + flagAsModified(); +} + +void CSVRender::UnpagedWorldspaceWidget::referenceAdded (const QModelIndex& parent, int start, + int end) +{ + if (mCell.get()) + if (mCell.get()->referenceAdded (parent, start, end)) + flagAsModified(); +} + +std::string CSVRender::UnpagedWorldspaceWidget::getStartupInstruction() +{ + Ogre::Vector3 position = getCamera()->getPosition(); + + std::ostringstream stream; + + stream + << "player->positionCell " + << position.x << ", " << position.y << ", " << position.z + << ", 0, \"" << mCellId << "\""; + + return stream.str(); +} + +CSVRender::WorldspaceWidget::dropRequirments CSVRender::UnpagedWorldspaceWidget::getDropRequirements (CSVRender::WorldspaceWidget::DropType type) const +{ + dropRequirments requirements = WorldspaceWidget::getDropRequirements (type); + + if (requirements!=ignored) + return requirements; + switch(type) { - case cellsInterior: + case Type_CellsInterior: return canHandle; - case cellsExterior: + case Type_CellsExterior: return needPaged; default: diff --git a/apps/opencs/view/render/unpagedworldspacewidget.hpp b/apps/opencs/view/render/unpagedworldspacewidget.hpp index bb5340845..5924abaa9 100644 --- a/apps/opencs/view/render/unpagedworldspacewidget.hpp +++ b/apps/opencs/view/render/unpagedworldspacewidget.hpp @@ -2,8 +2,10 @@ #define OPENCS_VIEW_UNPAGEDWORLDSPACEWIDGET_H #include +#include #include "worldspacewidget.hpp" +#include "cell.hpp" class QModelIndex; @@ -25,17 +27,42 @@ namespace CSVRender std::string mCellId; CSMWorld::IdTable *mCellsModel; + CSMWorld::IdTable *mReferenceablesModel; + std::auto_ptr mCell; void update(); + protected: + + virtual void addVisibilitySelectorButtons (CSVWidget::SceneToolToggle *tool); + public: UnpagedWorldspaceWidget (const std::string& cellId, CSMDoc::Document& document, QWidget *parent); - virtual dropRequirments getDropRequirements(dropType type) const; + virtual dropRequirments getDropRequirements(DropType type) const; + + /// \return Drop handled? + virtual bool handleDrop (const std::vector& data, + DropType type); + + private: + + virtual void referenceableDataChanged (const QModelIndex& topLeft, + const QModelIndex& bottomRight); + + virtual void referenceableAboutToBeRemoved (const QModelIndex& parent, int start, int end); + + virtual void referenceableAdded (const QModelIndex& index, int start, int end); + + virtual void referenceDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight); + + virtual void referenceAboutToBeRemoved (const QModelIndex& parent, int start, int end); + + virtual void referenceAdded (const QModelIndex& index, int start, int end); - virtual void handleDrop(const std::vector& data); + virtual std::string getStartupInstruction(); private slots: diff --git a/apps/opencs/view/render/worldspacewidget.cpp b/apps/opencs/view/render/worldspacewidget.cpp index 59b82bb67..6c6acd22d 100644 --- a/apps/opencs/view/render/worldspacewidget.cpp +++ b/apps/opencs/view/render/worldspacewidget.cpp @@ -1,24 +1,73 @@ #include "worldspacewidget.hpp" +#include + #include #include #include #include -#include "../world/scenetoolmode.hpp" -#include +#include "../../model/world/universalid.hpp" +#include "../../model/world/idtable.hpp" -CSVRender::WorldspaceWidget::WorldspaceWidget (const CSMDoc::Document& document, QWidget* parent) -: SceneWidget (parent), mDocument(document) -{ - Ogre::Entity* ent = getSceneManager()->createEntity("cube", Ogre::SceneManager::PT_CUBE); - ent->setMaterialName("BaseWhite"); +#include "../widget/scenetoolmode.hpp" +#include "../widget/scenetooltoggle.hpp" +#include "../widget/scenetoolrun.hpp" + +#include "../world/physicsmanager.hpp" +#include "../world/physicssystem.hpp" - getSceneManager()->getRootSceneNode()->attachObject(ent); +#include "elements.hpp" +#include "editmode.hpp" +CSVRender::WorldspaceWidget::WorldspaceWidget (CSMDoc::Document& document, QWidget* parent) +: SceneWidget (parent), mDocument(document), mSceneElements(0), mRun(0), mPhysics(0), mMouse(0), + mInteractionMask (0) +{ setAcceptDrops(true); + + QAbstractItemModel *referenceables = + document.getData().getTableModel (CSMWorld::UniversalId::Type_Referenceables); + + connect (referenceables, SIGNAL (dataChanged (const QModelIndex&, const QModelIndex&)), + this, SLOT (referenceableDataChanged (const QModelIndex&, const QModelIndex&))); + connect (referenceables, SIGNAL (rowsAboutToBeRemoved (const QModelIndex&, int, int)), + this, SLOT (referenceableAboutToBeRemoved (const QModelIndex&, int, int))); + connect (referenceables, SIGNAL (rowsInserted (const QModelIndex&, int, int)), + this, SLOT (referenceableAdded (const QModelIndex&, int, int))); + + QAbstractItemModel *references = + document.getData().getTableModel (CSMWorld::UniversalId::Type_References); + + connect (references, SIGNAL (dataChanged (const QModelIndex&, const QModelIndex&)), + this, SLOT (referenceDataChanged (const QModelIndex&, const QModelIndex&))); + connect (references, SIGNAL (rowsAboutToBeRemoved (const QModelIndex&, int, int)), + this, SLOT (referenceAboutToBeRemoved (const QModelIndex&, int, int))); + connect (references, SIGNAL (rowsInserted (const QModelIndex&, int, int)), + this, SLOT (referenceAdded (const QModelIndex&, int, int))); + + QAbstractItemModel *debugProfiles = + document.getData().getTableModel (CSMWorld::UniversalId::Type_DebugProfiles); + + connect (debugProfiles, SIGNAL (dataChanged (const QModelIndex&, const QModelIndex&)), + this, SLOT (debugProfileDataChanged (const QModelIndex&, const QModelIndex&))); + connect (debugProfiles, SIGNAL (rowsAboutToBeRemoved (const QModelIndex&, int, int)), + this, SLOT (debugProfileAboutToBeRemoved (const QModelIndex&, int, int))); + + // associate WorldSpaceWidgets (and their SceneManagers) with Documents + // then create physics if there is a new document + mPhysics = CSVWorld::PhysicsManager::instance()->addSceneWidget(document, this); + mPhysics->addSceneManager(getSceneManager(), this); + mMouse = new MouseState(this); +} + +CSVRender::WorldspaceWidget::~WorldspaceWidget () +{ + delete mMouse; + mPhysics->removeSceneManager(getSceneManager()); + CSVWorld::PhysicsManager::instance()->removeSceneWidget(this); } void CSVRender::WorldspaceWidget::selectNavigationMode (const std::string& mode) @@ -38,14 +87,39 @@ void CSVRender::WorldspaceWidget::selectDefaultNavigationMode() setNavigation (&m1st); } -CSVWorld::SceneToolMode *CSVRender::WorldspaceWidget::makeNavigationSelector ( - CSVWorld::SceneToolbar *parent) +CSVWidget::SceneToolMode *CSVRender::WorldspaceWidget::makeNavigationSelector ( + CSVWidget::SceneToolbar *parent) { - CSVWorld::SceneToolMode *tool = new CSVWorld::SceneToolMode (parent); + CSVWidget::SceneToolMode *tool = new CSVWidget::SceneToolMode (parent, "Camera Mode"); - tool->addButton (":door.png", "1st"); /// \todo replace icons - tool->addButton (":GMST.png", "free"); - tool->addButton (":Info.png", "orbit"); + /// \todo replace icons + /// \todo consider user-defined button-mapping + tool->addButton (":scenetoolbar/1st-person", "1st", + "First Person" + "
  • Mouse-Look while holding the left button
  • " + "
  • WASD movement keys
  • " + "
  • Mouse wheel moves the camera forawrd/backward
  • " + "
  • Stafing (also vertically) by holding the left mouse button and control
  • " + "
  • Camera is held upright
  • " + "
  • Hold shift to speed up movement
  • " + "
"); + tool->addButton (":scenetoolbar/free-camera", "free", + "Free Camera" + "
  • Mouse-Look while holding the left button
  • " + "
  • Stafing (also vertically) via WASD or by holding the left mouse button and control
  • " + "
  • Mouse wheel moves the camera forawrd/backward
  • " + "
  • Roll camera with Q and E keys
  • " + "
  • Hold shift to speed up movement
  • " + "
"); + tool->addButton (":scenetoolbar/orbiting-camera", "orbit", + "Orbiting Camera" + "
  • Always facing the centre point
  • " + "
  • Rotate around the centre point via WASD or by moving the mouse while holding the left button
  • " + "
  • Mouse wheel moves camera away or towards centre point but can not pass through it
  • " + "
  • Roll camera with Q and E keys
  • " + "
  • Stafing (also vertically) by holding the left mouse button and control (includes relocation of the centre point)
  • " + "
  • Hold shift to speed up movement
  • " + "
"); connect (tool, SIGNAL (modeChanged (const std::string&)), this, SLOT (selectNavigationMode (const std::string&))); @@ -53,59 +127,163 @@ CSVWorld::SceneToolMode *CSVRender::WorldspaceWidget::makeNavigationSelector ( return tool; } -CSVRender::WorldspaceWidget::dropType CSVRender::WorldspaceWidget::getDropType ( +CSVWidget::SceneToolToggle *CSVRender::WorldspaceWidget::makeSceneVisibilitySelector (CSVWidget::SceneToolbar *parent) +{ + mSceneElements= new CSVWidget::SceneToolToggle (parent, + "Scene Element Visibility", ":placeholder"); + + addVisibilitySelectorButtons (mSceneElements); + + mSceneElements->setSelection (0xffffffff); + + connect (mSceneElements, SIGNAL (selectionChanged()), + this, SLOT (elementSelectionChanged())); + + return mSceneElements; +} + +CSVWidget::SceneToolRun *CSVRender::WorldspaceWidget::makeRunTool ( + CSVWidget::SceneToolbar *parent) +{ + CSMWorld::IdTable& debugProfiles = dynamic_cast ( + *mDocument.getData().getTableModel (CSMWorld::UniversalId::Type_DebugProfiles)); + + std::vector profiles; + + int idColumn = debugProfiles.findColumnIndex (CSMWorld::Columns::ColumnId_Id); + int stateColumn = debugProfiles.findColumnIndex (CSMWorld::Columns::ColumnId_Modification); + int defaultColumn = debugProfiles.findColumnIndex ( + CSMWorld::Columns::ColumnId_DefaultProfile); + + int size = debugProfiles.rowCount(); + + for (int i=0; i& data) { - dropType output = notCells; - bool firstIteration = true; + DropType output = Type_Other; - for (unsigned i = 0; i < data.size(); ++i) + for (std::vector::const_iterator iter (data.begin()); + iter!=data.end(); ++iter) { - if (data[i].getType() == CSMWorld::UniversalId::Type_Cell || - data[i].getType() == CSMWorld::UniversalId::Type_Cell_Missing) + DropType type = Type_Other; + + if (iter->getType()==CSMWorld::UniversalId::Type_Cell || + iter->getType()==CSMWorld::UniversalId::Type_Cell_Missing) { - if (*(data[i].getId().begin()) == '#') //exterior - { - if (firstIteration) - { - output = cellsExterior; - firstIteration = false; - continue; - } - - if (output == cellsInterior) - { - output = cellsMixed; - break; - } else { - output = cellsInterior; - } - } else //interior - { - if (firstIteration) - { - output = cellsInterior; - firstIteration = false; - continue; - } - - if (output == cellsExterior) - { - output = cellsMixed; - break; - } else { - output = cellsInterior; - } - } - } else { - output = notCells; - break; + type = iter->getId().substr (0, 1)=="#" ? Type_CellsExterior : Type_CellsInterior; } + else if (iter->getType()==CSMWorld::UniversalId::Type_DebugProfile) + type = Type_DebugProfile; + + if (iter==data.begin()) + output = type; + else if (output!=type) // mixed types -> ignore + return Type_Other; } return output; } +CSVRender::WorldspaceWidget::dropRequirments + CSVRender::WorldspaceWidget::getDropRequirements (DropType type) const +{ + if (type==Type_DebugProfile) + return canHandle; + + return ignored; +} + +bool CSVRender::WorldspaceWidget::handleDrop (const std::vector& data, + DropType type) +{ + if (type==Type_DebugProfile) + { + if (mRun) + { + for (std::vector::const_iterator iter (data.begin()); + iter!=data.end(); ++iter) + mRun->addProfile (iter->getId()); + } + + return true; + } + + return false; +} + +unsigned int CSVRender::WorldspaceWidget::getVisibilityMask() const +{ + return mSceneElements->getSelection(); +} + +void CSVRender::WorldspaceWidget::setInteractionMask (unsigned int mask) +{ + mInteractionMask = mask | Element_CellMarker | Element_CellArrow; +} + +unsigned int CSVRender::WorldspaceWidget::getInteractionMask() const +{ + return mInteractionMask & getVisibilityMask(); +} + +void CSVRender::WorldspaceWidget::addVisibilitySelectorButtons ( + CSVWidget::SceneToolToggle *tool) +{ + tool->addButton (":placeholder", Element_Reference, ":placeholder", "References"); + tool->addButton (":placeholder", Element_Terrain, ":placeholder", "Terrain"); + tool->addButton (":placeholder", Element_Water, ":placeholder", "Water"); + tool->addButton (":placeholder", Element_Pathgrid, ":placeholder", "Pathgrid"); +} + +void CSVRender::WorldspaceWidget::addEditModeSelectorButtons (CSVWidget::SceneToolMode *tool) +{ + /// \todo replace EditMode with suitable subclasses + tool->addButton ( + new EditMode (this, QIcon (":placeholder"), Element_Reference, "Reference editing"), + "object"); + tool->addButton ( + new EditMode (this, QIcon (":placeholder"), Element_Pathgrid, "Pathgrid editing"), + "pathgrid"); +} + +CSMDoc::Document& CSVRender::WorldspaceWidget::getDocument() +{ + return mDocument; +} + void CSVRender::WorldspaceWidget::dragEnterEvent (QDragEnterEvent* event) { event->accept(); @@ -116,13 +294,140 @@ void CSVRender::WorldspaceWidget::dragMoveEvent(QDragMoveEvent *event) event->accept(); } - void CSVRender::WorldspaceWidget::dropEvent (QDropEvent* event) { const CSMWorld::TableMimeData* mime = dynamic_cast (event->mimeData()); + if (!mime) // May happen when non-records (e.g. plain text) are dragged and dropped + return; if (mime->fromDocument (mDocument)) { emit dataDropped(mime->getData()); } //not handling drops from different documents at the moment -} \ No newline at end of file +} + +void CSVRender::WorldspaceWidget::runRequest (const std::string& profile) +{ + mDocument.startRunning (profile, getStartupInstruction()); +} + +void CSVRender::WorldspaceWidget::debugProfileDataChanged (const QModelIndex& topLeft, + const QModelIndex& bottomRight) +{ + if (!mRun) + return; + + CSMWorld::IdTable& debugProfiles = dynamic_cast ( + *mDocument.getData().getTableModel (CSMWorld::UniversalId::Type_DebugProfiles)); + + int idColumn = debugProfiles.findColumnIndex (CSMWorld::Columns::ColumnId_Id); + int stateColumn = debugProfiles.findColumnIndex (CSMWorld::Columns::ColumnId_Modification); + + for (int i=topLeft.row(); i<=bottomRight.row(); ++i) + { + int state = debugProfiles.data (debugProfiles.index (i, stateColumn)).toInt(); + + // As of version 0.33 this case can not happen because debug profiles exist only in + // project or session scope, which means they will never be in deleted state. But we + // are adding the code for the sake of completeness and to avoid surprises if debug + // profile ever get extended to content scope. + if (state==CSMWorld::RecordBase::State_Deleted) + mRun->removeProfile (debugProfiles.data ( + debugProfiles.index (i, idColumn)).toString().toUtf8().constData()); + } +} + +void CSVRender::WorldspaceWidget::debugProfileAboutToBeRemoved (const QModelIndex& parent, + int start, int end) +{ + if (parent.isValid()) + return; + + if (!mRun) + return; + + CSMWorld::IdTable& debugProfiles = dynamic_cast ( + *mDocument.getData().getTableModel (CSMWorld::UniversalId::Type_DebugProfiles)); + + int idColumn = debugProfiles.findColumnIndex (CSMWorld::Columns::ColumnId_Id); + + for (int i=start; i<=end; ++i) + { + mRun->removeProfile (debugProfiles.data ( + debugProfiles.index (i, idColumn)).toString().toUtf8().constData()); + } +} + +void CSVRender::WorldspaceWidget::elementSelectionChanged() +{ + setVisibilityMask (getVisibilityMask()); + flagAsModified(); + updateOverlay(); +} + +void CSVRender::WorldspaceWidget::updateOverlay() +{ +} + +CSVWorld::PhysicsSystem *CSVRender::WorldspaceWidget::getPhysics() +{ + assert(mPhysics); + return mPhysics; +} + +void CSVRender::WorldspaceWidget::mouseMoveEvent (QMouseEvent *event) +{ + if(event->buttons() & Qt::RightButton) + { + mMouse->mouseMoveEvent(event); + } + SceneWidget::mouseMoveEvent(event); +} + +void CSVRender::WorldspaceWidget::mousePressEvent (QMouseEvent *event) +{ + if(event->buttons() & Qt::RightButton) + { + mMouse->mousePressEvent(event); + } + //SceneWidget::mousePressEvent(event); +} + +void CSVRender::WorldspaceWidget::mouseReleaseEvent (QMouseEvent *event) +{ + if(event->button() == Qt::RightButton) + { + if(!getViewport()) + { + SceneWidget::mouseReleaseEvent(event); + return; + } + mMouse->mouseReleaseEvent(event); + } + SceneWidget::mouseReleaseEvent(event); +} + +void CSVRender::WorldspaceWidget::mouseDoubleClickEvent (QMouseEvent *event) +{ + if(event->button() == Qt::RightButton) + { + mMouse->mouseDoubleClickEvent(event); + } + //SceneWidget::mouseDoubleClickEvent(event); +} + +void CSVRender::WorldspaceWidget::wheelEvent (QWheelEvent *event) +{ + if(!mMouse->wheelEvent(event)) + SceneWidget::wheelEvent(event); +} + +void CSVRender::WorldspaceWidget::keyPressEvent (QKeyEvent *event) +{ + if(event->key() == Qt::Key_Escape) + { + mMouse->cancelDrag(); + } + else + SceneWidget::keyPressEvent(event); +} diff --git a/apps/opencs/view/render/worldspacewidget.hpp b/apps/opencs/view/render/worldspacewidget.hpp index a14e03915..e54eb91e1 100644 --- a/apps/opencs/view/render/worldspacewidget.hpp +++ b/apps/opencs/view/render/worldspacewidget.hpp @@ -2,6 +2,7 @@ #define OPENCS_VIEW_WORLDSPACEWIDGET_H #include "scenewidget.hpp" +#include "mousestate.hpp" #include "navigation1st.hpp" #include "navigationfree.hpp" @@ -13,10 +14,18 @@ namespace CSMWorld { class UniversalId; } -namespace CSVWorld + +namespace CSVWidget { class SceneToolMode; + class SceneToolToggle; class SceneToolbar; + class SceneToolRun; +} + +namespace CSVWorld +{ + class PhysicsSystem; } namespace CSVRender @@ -28,15 +37,21 @@ namespace CSVRender CSVRender::Navigation1st m1st; CSVRender::NavigationFree mFree; CSVRender::NavigationOrbit mOrbit; + CSVWidget::SceneToolToggle *mSceneElements; + CSVWidget::SceneToolRun *mRun; + CSMDoc::Document& mDocument; + CSVWorld::PhysicsSystem *mPhysics; + MouseState *mMouse; + unsigned int mInteractionMask; public: - enum dropType + enum DropType { - cellsMixed, - cellsInterior, - cellsExterior, - notCells + Type_CellsInterior, + Type_CellsExterior, + Type_Other, + Type_DebugProfile }; enum dropRequirments @@ -47,25 +62,67 @@ namespace CSVRender ignored //either mixed cells, or not cells }; - WorldspaceWidget (const CSMDoc::Document& document, QWidget *parent = 0); + WorldspaceWidget (CSMDoc::Document& document, QWidget *parent = 0); + ~WorldspaceWidget (); - CSVWorld::SceneToolMode *makeNavigationSelector (CSVWorld::SceneToolbar *parent); + CSVWidget::SceneToolMode *makeNavigationSelector (CSVWidget::SceneToolbar *parent); ///< \attention The created tool is not added to the toolbar (via addTool). Doing that /// is the responsibility of the calling function. + /// \attention The created tool is not added to the toolbar (via addTool). Doing + /// that is the responsibility of the calling function. + CSVWidget::SceneToolToggle *makeSceneVisibilitySelector ( + CSVWidget::SceneToolbar *parent); + + /// \attention The created tool is not added to the toolbar (via addTool). Doing + /// that is the responsibility of the calling function. + CSVWidget::SceneToolRun *makeRunTool (CSVWidget::SceneToolbar *parent); + + /// \attention The created tool is not added to the toolbar (via addTool). Doing + /// that is the responsibility of the calling function. + CSVWidget::SceneToolMode *makeEditModeSelector (CSVWidget::SceneToolbar *parent); + void selectDefaultNavigationMode(); - static dropType getDropType(const std::vector& data); + static DropType getDropType(const std::vector& data); - virtual dropRequirments getDropRequirements(dropType type) const = 0; + virtual dropRequirments getDropRequirements(DropType type) const; virtual void useViewHint (const std::string& hint); ///< Default-implementation: ignored. - virtual void handleDrop(const std::vector& data) = 0; + /// \return Drop handled? + virtual bool handleDrop (const std::vector& data, + DropType type); + + virtual unsigned int getVisibilityMask() const; + + /// \note This function will implicitly add elements that are independent of the + /// selected edit mode. + virtual void setInteractionMask (unsigned int mask); + + /// \note This function will only return those elements that are both visible and + /// marked for interaction. + unsigned int getInteractionMask() const; protected: - const CSMDoc::Document& mDocument; //for checking if drop comes from same document + + virtual void addVisibilitySelectorButtons (CSVWidget::SceneToolToggle *tool); + + virtual void addEditModeSelectorButtons (CSVWidget::SceneToolMode *tool); + + CSMDoc::Document& getDocument(); + + virtual void updateOverlay(); + + CSVWorld::PhysicsSystem *getPhysics(); + + virtual void mouseMoveEvent (QMouseEvent *event); + virtual void mousePressEvent (QMouseEvent *event); + virtual void mouseReleaseEvent (QMouseEvent *event); + virtual void mouseDoubleClickEvent (QMouseEvent *event); + virtual void wheelEvent (QWheelEvent *event); + virtual void keyPressEvent (QKeyEvent *event); private: @@ -75,15 +132,45 @@ namespace CSVRender void dragMoveEvent(QDragMoveEvent *event); + virtual std::string getStartupInstruction() = 0; + private slots: void selectNavigationMode (const std::string& mode); + virtual void referenceableDataChanged (const QModelIndex& topLeft, + const QModelIndex& bottomRight) = 0; + + virtual void referenceableAboutToBeRemoved (const QModelIndex& parent, int start, int end) = 0; + + virtual void referenceableAdded (const QModelIndex& index, int start, int end) = 0; + + virtual void referenceDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) = 0; + + virtual void referenceAboutToBeRemoved (const QModelIndex& parent, int start, int end) = 0; + + virtual void referenceAdded (const QModelIndex& index, int start, int end) = 0; + + virtual void runRequest (const std::string& profile); + + void debugProfileDataChanged (const QModelIndex& topLeft, + const QModelIndex& bottomRight); + + void debugProfileAboutToBeRemoved (const QModelIndex& parent, int start, int end); + + + protected slots: + + void elementSelectionChanged(); + signals: void closeRequest(); + void dataDropped(const std::vector& data); + + friend class MouseState; }; } -#endif \ No newline at end of file +#endif diff --git a/apps/opencs/view/settings/booleanview.cpp b/apps/opencs/view/settings/booleanview.cpp index 2a3f0cba6..29f9775af 100644 --- a/apps/opencs/view/settings/booleanview.cpp +++ b/apps/opencs/view/settings/booleanview.cpp @@ -12,16 +12,27 @@ CSVSettings::BooleanView::BooleanView (CSMSettings::Setting *setting, Page *parent) - : View (setting, parent) + : mType(setting->type()), View (setting, parent) { foreach (const QString &value, setting->declaredValues()) { QAbstractButton *button = 0; - switch (setting->type()) + switch (mType) { - case CSMSettings::Type_CheckBox: - button = new QCheckBox (value, this); + case CSMSettings::Type_CheckBox: { + if(mButtons.empty()) // show only one for checkboxes + { + button = new QCheckBox (value, this); + button->setChecked (setting->defaultValues().at(0) == "true" ? true : false); + + // special visual treatment option for checkboxes + if(setting->specialValueText() != "") { + Frame::setTitle(""); + button->setText(setting->specialValueText()); + } + } + } break; case CSMSettings::Type_RadioButton: @@ -32,14 +43,17 @@ CSVSettings::BooleanView::BooleanView (CSMSettings::Setting *setting, break; } - connect (button, SIGNAL (clicked (bool)), - this, SLOT (slotToggled (bool))); + if(button && (mType != CSMSettings::Type_CheckBox || mButtons.empty())) + { + connect (button, SIGNAL (clicked (bool)), + this, SLOT (slotToggled (bool))); - button->setObjectName (value); + button->setObjectName (value); - addWidget (button); + addWidget (button); - mButtons[value] = button; + mButtons[value] = button; + } } } @@ -53,8 +67,14 @@ void CSVSettings::BooleanView::slotToggled (bool state) foreach (QString key, mButtons.keys()) { - if (mButtons.value(key)->isChecked()) - values.append (key); + // checkbox values are true/false unlike radio buttons + if(mType == CSMSettings::Type_CheckBox) + values.append(mButtons.value(key)->isChecked() ? "true" : "false"); + else + { + if (mButtons.value(key)->isChecked()) + values.append (key); + } } setSelectedValues (values, false); diff --git a/apps/opencs/view/settings/booleanview.hpp b/apps/opencs/view/settings/booleanview.hpp index 55ef0bb08..53198234a 100644 --- a/apps/opencs/view/settings/booleanview.hpp +++ b/apps/opencs/view/settings/booleanview.hpp @@ -16,6 +16,7 @@ namespace CSVSettings Q_OBJECT QMap mButtons; + enum CSMSettings::SettingType mType; public: explicit BooleanView (CSMSettings::Setting *setting, diff --git a/apps/opencs/view/settings/dialog.cpp b/apps/opencs/view/settings/dialog.cpp index 56bc1fdfe..0b1231266 100644 --- a/apps/opencs/view/settings/dialog.cpp +++ b/apps/opencs/view/settings/dialog.cpp @@ -1,10 +1,13 @@ #include "dialog.hpp" +#include + #include #include #include #include #include +#include #include "../../model/settings/usersettings.hpp" @@ -12,8 +15,6 @@ #include -#include - #include #include #include @@ -26,6 +27,10 @@ CSVSettings::Dialog::Dialog(QMainWindow *parent) { setWindowTitle(QString::fromUtf8 ("User Settings")); + setSizePolicy (QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); + + setMinimumSize (600, 400); + setupDialog(); connect (mPageListWidget, @@ -39,20 +44,14 @@ void CSVSettings::Dialog::slotChangePage { mStackedWidget->changePage (mPageListWidget->row (cur), mPageListWidget->row (prev)); - - layout()->activate(); - setFixedSize(minimumSizeHint()); } void CSVSettings::Dialog::setupDialog() { - //create central widget with it's layout and immediate children - QWidget *centralWidget = new QGroupBox (this); + QSplitter *centralWidget = new QSplitter (this); + centralWidget->setSizePolicy (QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); - centralWidget->setLayout (new QHBoxLayout()); - centralWidget->setSizePolicy (QSizePolicy::Expanding, QSizePolicy::Preferred); setCentralWidget (centralWidget); - setDockOptions (QMainWindow::AllowNestedDocks); buildPageListWidget (centralWidget); buildStackedWidget (centralWidget); @@ -64,37 +63,39 @@ void CSVSettings::Dialog::buildPages() QFontMetrics fm (QApplication::font()); + int maxWidth = 1; + foreach (Page *page, SettingWindow::pages()) { - QString pageName = page->objectName(); + maxWidth = std::max (maxWidth, fm.width(page->getLabel())); - int textWidth = fm.width(pageName); + new QListWidgetItem (page->getLabel(), mPageListWidget); - new QListWidgetItem (pageName, mPageListWidget); - mPageListWidget->setFixedWidth (textWidth + 50); - - mStackedWidget->addWidget (&dynamic_cast(*(page))); + mStackedWidget->addWidget (page); } + mPageListWidget->setMaximumWidth (maxWidth + 10); + resize (mStackedWidget->sizeHint()); } -void CSVSettings::Dialog::buildPageListWidget (QWidget *centralWidget) +void CSVSettings::Dialog::buildPageListWidget (QSplitter *centralWidget) { mPageListWidget = new QListWidget (centralWidget); mPageListWidget->setMinimumWidth(50); - mPageListWidget->setSizePolicy (QSizePolicy::Fixed, QSizePolicy::Expanding); + mPageListWidget->setSizePolicy (QSizePolicy::Expanding, QSizePolicy::Expanding); mPageListWidget->setSelectionBehavior (QAbstractItemView::SelectItems); - centralWidget->layout()->addWidget(mPageListWidget); + centralWidget->addWidget(mPageListWidget); } -void CSVSettings::Dialog::buildStackedWidget (QWidget *centralWidget) +void CSVSettings::Dialog::buildStackedWidget (QSplitter *centralWidget) { mStackedWidget = new ResizeableStackedWidget (centralWidget); + mStackedWidget->setSizePolicy (QSizePolicy::Preferred, QSizePolicy::Expanding); - centralWidget->layout()->addWidget (mStackedWidget); + centralWidget->addWidget (mStackedWidget); } void CSVSettings::Dialog::closeEvent (QCloseEvent *event) @@ -114,8 +115,20 @@ void CSVSettings::Dialog::show() setViewValues(); } - QPoint screenCenter = QApplication::desktop()->screenGeometry().center(); - - move (screenCenter - geometry().center()); + QWidget *currView = QApplication::activeWindow(); + if(currView) + { + // place at the center of the window with focus + QSize size = currView->size(); + move(currView->geometry().x()+(size.width() - frameGeometry().width())/2, + currView->geometry().y()+(size.height() - frameGeometry().height())/2); + } + else + { + // something's gone wrong, place at the center of the screen + QPoint screenCenter = QApplication::desktop()->screenGeometry().center(); + move(screenCenter - QPoint(frameGeometry().width()/2, + frameGeometry().height()/2)); + } QWidget::show(); } diff --git a/apps/opencs/view/settings/dialog.hpp b/apps/opencs/view/settings/dialog.hpp index b0e12c461..cb85bddb9 100644 --- a/apps/opencs/view/settings/dialog.hpp +++ b/apps/opencs/view/settings/dialog.hpp @@ -8,6 +8,7 @@ class QStackedWidget; class QListWidget; class QListWidgetItem; +class QSplitter; namespace CSVSettings { @@ -39,8 +40,8 @@ namespace CSVSettings { private: void buildPages(); - void buildPageListWidget (QWidget *centralWidget); - void buildStackedWidget (QWidget *centralWidget); + void buildPageListWidget (QSplitter *centralWidget); + void buildStackedWidget (QSplitter *centralWidget); public slots: diff --git a/apps/opencs/view/settings/frame.cpp b/apps/opencs/view/settings/frame.cpp index 019024776..32e094274 100644 --- a/apps/opencs/view/settings/frame.cpp +++ b/apps/opencs/view/settings/frame.cpp @@ -3,7 +3,7 @@ #include const QString CSVSettings::Frame::sInvisibleBoxStyle = - QString::fromUtf8("Frame { border:2px; padding 2px; margin: 2px;}"); + QString::fromUtf8("Frame { border:2px; padding: 2px; margin: 2px;}"); CSVSettings::Frame::Frame (bool isVisible, const QString &title, QWidget *parent) @@ -14,7 +14,10 @@ CSVSettings::Frame::Frame (bool isVisible, const QString &title, mVisibleBoxStyle = styleSheet(); if (!isVisible) + { + // must be Page, not a View setStyleSheet (sInvisibleBoxStyle); + } setLayout (mLayout); } @@ -35,7 +38,7 @@ void CSVSettings::Frame::hideWidgets() QWidget *widg = static_cast (obj); if (widg->property("sizePolicy").isValid()) - widg->setSizePolicy (QSizePolicy::Ignored, QSizePolicy::Ignored); + widg->setSizePolicy (QSizePolicy::Expanding, QSizePolicy::Expanding); } layout()->activate(); diff --git a/apps/opencs/view/settings/page.cpp b/apps/opencs/view/settings/page.cpp index afd4bff5e..e846840b8 100644 --- a/apps/opencs/view/settings/page.cpp +++ b/apps/opencs/view/settings/page.cpp @@ -1,4 +1,7 @@ #include "page.hpp" + +#include + #include "view.hpp" #include "booleanview.hpp" #include "textview.hpp" @@ -14,10 +17,9 @@ QMap CSVSettings::Page::mViewFactories; -CSVSettings::Page::Page(const QString &pageName, - QList settingList, - SettingWindow *parent) : - mParent(parent), mIsEditorPage (false), Frame(false, "", parent) +CSVSettings::Page::Page (const QString &pageName, QList settingList, + SettingWindow *parent, const QString& label) +: mParent(parent), mIsEditorPage (false), Frame(false, "", parent), mLabel (label) { setObjectName (pageName); @@ -38,7 +40,18 @@ void CSVSettings::Page::setupViews void CSVSettings::Page::addView (CSMSettings::Setting *setting) { if (setting->viewType() == ViewType_Undefined) - return; + { + if(setting->specialValueText() != "") + { + // hack to put a label + addWidget(new QLabel(setting->specialValueText()), + setting->viewRow(), setting->viewColumn(), + setting->rowSpan(), setting->columnSpan()); + return; + } + else + return; + } View *view = mViewFactories[setting->viewType()]->createView(setting, this); @@ -90,3 +103,8 @@ void CSVSettings::Page::buildFactories() mViewFactories[ViewType_List] = new ListViewFactory (this); mViewFactories[ViewType_Range] = new RangeViewFactory (this); } + +QString CSVSettings::Page::getLabel() const +{ + return mLabel; +} diff --git a/apps/opencs/view/settings/page.hpp b/apps/opencs/view/settings/page.hpp index 877d4bef8..caf2eec3f 100644 --- a/apps/opencs/view/settings/page.hpp +++ b/apps/opencs/view/settings/page.hpp @@ -25,11 +25,11 @@ namespace CSVSettings SettingWindow *mParent; static QMap mViewFactories; bool mIsEditorPage; + QString mLabel; public: - explicit Page(const QString &pageName, - QList settingList, - SettingWindow *parent); + Page (const QString &pageName, QList settingList, + SettingWindow *parent, const QString& label); ///Creates a new view based on the passed setting and adds it to ///the page. @@ -42,6 +42,8 @@ namespace CSVSettings ///returns the list of views associated with the page const QList &views () const { return mViews; } + QString getLabel() const; + private: ///Creates views based on the passed setting list diff --git a/apps/opencs/view/settings/rangeview.cpp b/apps/opencs/view/settings/rangeview.cpp index 8ae6caca0..246f7ece2 100644 --- a/apps/opencs/view/settings/rangeview.cpp +++ b/apps/opencs/view/settings/rangeview.cpp @@ -36,8 +36,11 @@ CSVSettings::RangeView::RangeView (CSMSettings::Setting *setting, break; } - mRangeWidget->setFixedWidth (widgetWidth (setting->widgetWidth())); - mRangeWidget->setObjectName (setting->name()); + if(mRangeWidget) + { + mRangeWidget->setFixedWidth (widgetWidth (setting->widgetWidth())); + mRangeWidget->setObjectName (setting->name()); + } addWidget (mRangeWidget); } @@ -75,13 +78,16 @@ void CSVSettings::RangeView::buildSlider (CSMSettings::Setting *setting) break; } - mRangeWidget->setProperty ("minimum", setting->minimum()); - mRangeWidget->setProperty ("maximum", setting->maximum()); - mRangeWidget->setProperty ("tracking", false); - mRangeWidget->setProperty ("singleStep", setting->singleStep()); + if(mRangeWidget) + { + mRangeWidget->setProperty ("minimum", setting->minimum()); + mRangeWidget->setProperty ("maximum", setting->maximum()); + mRangeWidget->setProperty ("tracking", false); + mRangeWidget->setProperty ("singleStep", setting->singleStep()); - connect (mRangeWidget, SIGNAL (valueChanged (int)), - this, SLOT (slotUpdateView (int))); + connect (mRangeWidget, SIGNAL (valueChanged (int)), + this, SLOT (slotUpdateView (int))); + } } void CSVSettings::RangeView::buildSpinBox (CSMSettings::Setting *setting) @@ -111,7 +117,7 @@ void CSVSettings::RangeView::buildSpinBox (CSMSettings::Setting *setting) break; default: - break; + return; } //min / max values are set automatically in AlphaSpinBox @@ -120,14 +126,15 @@ void CSVSettings::RangeView::buildSpinBox (CSMSettings::Setting *setting) mRangeWidget->setProperty ("minimum", setting->minimum()); mRangeWidget->setProperty ("maximum", setting->maximum()); mRangeWidget->setProperty ("singleStep", setting->singleStep()); - mRangeWidget->setProperty ("specialValueText", - setting->specialValueText()); } mRangeWidget->setProperty ("prefix", setting->prefix()); mRangeWidget->setProperty ("suffix", setting->suffix()); mRangeWidget->setProperty ("wrapping", setting->wrapping()); + dynamic_cast (mRangeWidget)->setAlignment(Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter); + if(setting->type() == CSMSettings::Type_SpinBox && setting->declaredValues().isEmpty()) + dynamic_cast (mRangeWidget)->setValue (setting->defaultValues().at(0).toInt()); } void CSVSettings::RangeView::slotUpdateView (int value) diff --git a/apps/opencs/view/settings/resizeablestackedwidget.cpp b/apps/opencs/view/settings/resizeablestackedwidget.cpp index cb127cb76..0e87a2506 100644 --- a/apps/opencs/view/settings/resizeablestackedwidget.cpp +++ b/apps/opencs/view/settings/resizeablestackedwidget.cpp @@ -34,7 +34,6 @@ void CSVSettings::ResizeableStackedWidget::changePage curPage->showWidgets(); layout()->activate(); - setFixedSize(minimumSizeHint()); setCurrentIndex (current); } diff --git a/apps/opencs/view/settings/settingwindow.cpp b/apps/opencs/view/settings/settingwindow.cpp index 7cdf2bded..76ea9dc4f 100644 --- a/apps/opencs/view/settings/settingwindow.cpp +++ b/apps/opencs/view/settings/settingwindow.cpp @@ -9,7 +9,7 @@ #include "view.hpp" CSVSettings::SettingWindow::SettingWindow(QWidget *parent) - : QMainWindow(parent) + : QMainWindow(parent), mModel(NULL) {} void CSVSettings::SettingWindow::createPages() @@ -19,10 +19,10 @@ void CSVSettings::SettingWindow::createPages() QList connectedSettings; foreach (const QString &pageName, pageMap.keys()) - { - QList pageSettings = pageMap.value (pageName); + { + QList pageSettings = pageMap.value (pageName).second; - mPages.append (new Page (pageName, pageSettings, this)); + mPages.append (new Page (pageName, pageSettings, this, pageMap.value (pageName).first)); for (int i = 0; i < pageSettings.size(); i++) { @@ -84,7 +84,7 @@ void CSVSettings::SettingWindow::createConnections void CSVSettings::SettingWindow::setViewValues() { - //iterate each page and view, setting their definintions + //iterate each page and view, setting their definitions //if they exist in the model foreach (const Page *page, mPages) { @@ -129,7 +129,3 @@ void CSVSettings::SettingWindow::saveSettings() mModel->saveDefinitions(); } -void CSVSettings::SettingWindow::closeEvent (QCloseEvent *event) -{ - QApplication::focusWidget()->clearFocus(); -} diff --git a/apps/opencs/view/settings/settingwindow.hpp b/apps/opencs/view/settings/settingwindow.hpp index 2266f130d..11bceee96 100644 --- a/apps/opencs/view/settings/settingwindow.hpp +++ b/apps/opencs/view/settings/settingwindow.hpp @@ -36,8 +36,6 @@ namespace CSVSettings { protected: - virtual void closeEvent (QCloseEvent *event); - ///construct the pages to be displayed in the dialog void createPages(); diff --git a/apps/opencs/view/settings/spinbox.cpp b/apps/opencs/view/settings/spinbox.cpp index 4b1447f8f..c70fc36d1 100644 --- a/apps/opencs/view/settings/spinbox.cpp +++ b/apps/opencs/view/settings/spinbox.cpp @@ -24,7 +24,7 @@ QString CSVSettings::SpinBox::textFromValue(int val) const int CSVSettings::SpinBox::valueFromText(const QString &text) const { if (mValueList.isEmpty()) - return -1; + return text.toInt(); // TODO: assumed integer, untested error handling for alpha types if (mValueList.contains (text)) return mValueList.indexOf(text); diff --git a/apps/opencs/view/settings/view.cpp b/apps/opencs/view/settings/view.cpp index 69109e2b3..39c7f89b2 100644 --- a/apps/opencs/view/settings/view.cpp +++ b/apps/opencs/view/settings/view.cpp @@ -17,11 +17,17 @@ CSVSettings::View::View(CSMSettings::Setting *setting, mIsMultiValue (setting->isMultiValue()), mViewKey (setting->page() + '/' + setting->name()), mSerializable (setting->serializable()), - Frame(true, setting->name(), parent) + Frame(true, setting->getLabel(), parent) { + if (!setting->getToolTip().isEmpty()) + setToolTip (setting->getToolTip()); + setObjectName (setting->name()); buildView(); buildModel (setting); + // apply stylesheet to view's frame if exists + if(setting->styleSheet() != "") + Frame::setStyleSheet (setting->styleSheet()); } void CSVSettings::View::buildModel (const CSMSettings::Setting *setting) diff --git a/apps/opencs/view/tools/reportsubview.cpp b/apps/opencs/view/tools/reportsubview.cpp index e84f5cf4b..5a2523789 100644 --- a/apps/opencs/view/tools/reportsubview.cpp +++ b/apps/opencs/view/tools/reportsubview.cpp @@ -21,7 +21,7 @@ CSVTools::ReportSubView::ReportSubView (const CSMWorld::UniversalId& id, CSMDoc: mTable->setSelectionMode (QAbstractItemView::ExtendedSelection); mIdTypeDelegate = CSVWorld::IdTypeDelegateFactory().makeDelegate ( - document.getUndoStack(), this); + document, this); mTable->setItemDelegateForColumn (0, mIdTypeDelegate); diff --git a/apps/opencs/view/widget/modebutton.cpp b/apps/opencs/view/widget/modebutton.cpp new file mode 100644 index 000000000..56896b422 --- /dev/null +++ b/apps/opencs/view/widget/modebutton.cpp @@ -0,0 +1,10 @@ + +#include "modebutton.hpp" + +CSVWidget::ModeButton::ModeButton (const QIcon& icon, const QString& tooltip, QWidget *parent) +: PushButton (icon, Type_Mode, tooltip, parent) +{} + +void CSVWidget::ModeButton::activate (SceneToolbar *toolbar) {} + +void CSVWidget::ModeButton::deactivate (SceneToolbar *toolbar) {} diff --git a/apps/opencs/view/widget/modebutton.hpp b/apps/opencs/view/widget/modebutton.hpp new file mode 100644 index 000000000..ac14afc95 --- /dev/null +++ b/apps/opencs/view/widget/modebutton.hpp @@ -0,0 +1,28 @@ +#ifndef CSV_WIDGET_MODEBUTTON_H +#define CSV_WIDGET_MODEBUTTON_H + +#include "pushbutton.hpp" + +namespace CSVWidget +{ + class SceneToolbar; + + /// \brief Specialist PushButton of Type_Mode for use in SceneToolMode + class ModeButton : public PushButton + { + Q_OBJECT + + public: + + ModeButton (const QIcon& icon, const QString& tooltip = "", + QWidget *parent = 0); + + /// Default-Implementation: do nothing + virtual void activate (SceneToolbar *toolbar); + + /// Default-Implementation: do nothing + virtual void deactivate (SceneToolbar *toolbar); + }; +} + +#endif diff --git a/apps/opencs/view/widget/pushbutton.cpp b/apps/opencs/view/widget/pushbutton.cpp new file mode 100644 index 000000000..f23462585 --- /dev/null +++ b/apps/opencs/view/widget/pushbutton.cpp @@ -0,0 +1,99 @@ + +#include "pushbutton.hpp" + +#include +#include + +void CSVWidget::PushButton::setExtendedToolTip() +{ + QString tooltip = mToolTip; + + if (tooltip.isEmpty()) + tooltip = "(Tool tip not implemented yet)"; + + switch (mType) + { + case Type_TopMode: + + tooltip += + "

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

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

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

shift-left click to "; + tooltip += isChecked() ? "disable" : "enable"; + tooltip += " and keep panel open)"; + + break; + } + + setToolTip (tooltip); +} + +void CSVWidget::PushButton::keyPressEvent (QKeyEvent *event) +{ + if (event->key()!=Qt::Key_Shift) + mKeepOpen = false; + + QPushButton::keyPressEvent (event); +} + +void CSVWidget::PushButton::keyReleaseEvent (QKeyEvent *event) +{ + if (event->key()==Qt::Key_Space) + mKeepOpen = event->modifiers() & Qt::ShiftModifier; + + QPushButton::keyReleaseEvent (event); +} + +void CSVWidget::PushButton::mouseReleaseEvent (QMouseEvent *event) +{ + mKeepOpen = event->button()==Qt::LeftButton && (event->modifiers() & Qt::ShiftModifier); + QPushButton::mouseReleaseEvent (event); +} + +CSVWidget::PushButton::PushButton (const QIcon& icon, Type type, const QString& tooltip, + QWidget *parent) +: QPushButton (icon, "", parent), mKeepOpen (false), mType (type), mToolTip (tooltip) +{ + setCheckable (type==Type_Mode || type==Type_Toggle); + setExtendedToolTip(); +} + +CSVWidget::PushButton::PushButton (Type type, const QString& tooltip, QWidget *parent) +: QPushButton (parent), mKeepOpen (false), mType (type), mToolTip (tooltip) +{ + setCheckable (type==Type_Mode || type==Type_Toggle); + setExtendedToolTip(); +} + +bool CSVWidget::PushButton::hasKeepOpen() const +{ + return mKeepOpen; +} + +QString CSVWidget::PushButton::getBaseToolTip() const +{ + return mToolTip; +} + +CSVWidget::PushButton::Type CSVWidget::PushButton::getType() const +{ + return mType; +} \ No newline at end of file diff --git a/apps/opencs/view/widget/pushbutton.hpp b/apps/opencs/view/widget/pushbutton.hpp new file mode 100644 index 000000000..35062a137 --- /dev/null +++ b/apps/opencs/view/widget/pushbutton.hpp @@ -0,0 +1,59 @@ +#ifndef CSV_WIDGET_PUSHBUTTON_H +#define CSV_WIDGET_PUSHBUTTON_H + +#include + +namespace CSVWidget +{ + class PushButton : public QPushButton + { + Q_OBJECT + + public: + + enum Type + { + Type_TopMode, // top level button for mode selector panel + Type_TopAction, // top level button that triggers an action + Type_Mode, // mode button + Type_Toggle + }; + + private: + + bool mKeepOpen; + Type mType; + QString mToolTip; + + private: + + void setExtendedToolTip(); + + protected: + + virtual void keyPressEvent (QKeyEvent *event); + + virtual void keyReleaseEvent (QKeyEvent *event); + + virtual void mouseReleaseEvent (QMouseEvent *event); + + public: + + /// \param push Do not maintain a toggle state + PushButton (const QIcon& icon, Type type, const QString& tooltip = "", + QWidget *parent = 0); + + /// \param push Do not maintain a toggle state + PushButton (Type type, const QString& tooltip = "", + QWidget *parent = 0); + + bool hasKeepOpen() const; + + /// Return tooltip used at construction (without any button-specific modifications) + QString getBaseToolTip() const; + + Type getType() const; + }; +} + +#endif diff --git a/apps/opencs/view/widget/scenetool.cpp b/apps/opencs/view/widget/scenetool.cpp new file mode 100644 index 000000000..b8e9f895f --- /dev/null +++ b/apps/opencs/view/widget/scenetool.cpp @@ -0,0 +1,34 @@ + +#include "scenetool.hpp" + +#include + +#include "scenetoolbar.hpp" + +CSVWidget::SceneTool::SceneTool (SceneToolbar *parent, Type type) +: PushButton (type, "", parent) +{ + setSizePolicy (QSizePolicy (QSizePolicy::Fixed, QSizePolicy::Fixed)); + setIconSize (QSize (parent->getIconSize(), parent->getIconSize())); + setFixedSize (parent->getButtonSize(), parent->getButtonSize()); + + connect (this, SIGNAL (clicked()), this, SLOT (openRequest())); +} + +void CSVWidget::SceneTool::activate() {} + +void CSVWidget::SceneTool::mouseReleaseEvent (QMouseEvent *event) +{ + if (getType()==Type_TopAction && event->button()==Qt::RightButton) + showPanel (parentWidget()->mapToGlobal (pos())); + else + PushButton::mouseReleaseEvent (event); +} + +void CSVWidget::SceneTool::openRequest() +{ + if (getType()==Type_TopAction) + activate(); + else + showPanel (parentWidget()->mapToGlobal (pos())); +} diff --git a/apps/opencs/view/widget/scenetool.hpp b/apps/opencs/view/widget/scenetool.hpp new file mode 100644 index 000000000..cdea88096 --- /dev/null +++ b/apps/opencs/view/widget/scenetool.hpp @@ -0,0 +1,35 @@ +#ifndef CSV_WIDGET_SCENETOOL_H +#define CSV_WIDGET_SCENETOOL_H + +#include "pushbutton.hpp" + +namespace CSVWidget +{ + class SceneToolbar; + + ///< \brief Tool base class + class SceneTool : public PushButton + { + Q_OBJECT + + public: + + SceneTool (SceneToolbar *parent, Type type = Type_TopMode); + + virtual void showPanel (const QPoint& position) = 0; + + /// This function will only called for buttons of type Type_TopAction. The default + /// implementation is empty. + virtual void activate(); + + protected: + + void mouseReleaseEvent (QMouseEvent *event); + + private slots: + + void openRequest(); + }; +} + +#endif diff --git a/apps/opencs/view/widget/scenetoolbar.cpp b/apps/opencs/view/widget/scenetoolbar.cpp new file mode 100644 index 000000000..f7023b31f --- /dev/null +++ b/apps/opencs/view/widget/scenetoolbar.cpp @@ -0,0 +1,58 @@ + +#include "scenetoolbar.hpp" + +#include +#include + +#include "scenetool.hpp" + +void CSVWidget::SceneToolbar::focusInEvent (QFocusEvent *event) +{ + QWidget::focusInEvent (event); + + if (mLayout->count()) + dynamic_cast (*mLayout->itemAt (0)).widget()->setFocus(); +} + +CSVWidget::SceneToolbar::SceneToolbar (int buttonSize, QWidget *parent) +: QWidget (parent), mButtonSize (buttonSize), mIconSize (buttonSize-6) +{ + setFixedWidth (mButtonSize); + + mLayout = new QVBoxLayout (this); + mLayout->setAlignment (Qt::AlignTop); + + mLayout->setContentsMargins (QMargins (0, 0, 0, 0)); + + setLayout (mLayout); + + /// \todo make shortcut configurable + QShortcut *focusScene = new QShortcut (Qt::Key_T, this, 0, 0, Qt::WidgetWithChildrenShortcut); + connect (focusScene, SIGNAL (activated()), this, SIGNAL (focusSceneRequest())); +} + +void CSVWidget::SceneToolbar::addTool (SceneTool *tool, SceneTool *insertPoint) +{ + if (!insertPoint) + mLayout->addWidget (tool, 0, Qt::AlignTop); + else + { + int index = mLayout->indexOf (insertPoint); + mLayout->insertWidget (index+1, tool, 0, Qt::AlignTop); + } +} + +void CSVWidget::SceneToolbar::removeTool (SceneTool *tool) +{ + mLayout->removeWidget (tool); +} + +int CSVWidget::SceneToolbar::getButtonSize() const +{ + return mButtonSize; +} + +int CSVWidget::SceneToolbar::getIconSize() const +{ + return mIconSize; +} diff --git a/apps/opencs/view/widget/scenetoolbar.hpp b/apps/opencs/view/widget/scenetoolbar.hpp new file mode 100644 index 000000000..8e2c8ab00 --- /dev/null +++ b/apps/opencs/view/widget/scenetoolbar.hpp @@ -0,0 +1,44 @@ +#ifndef CSV_WIDGET_SCENETOOLBAR_H +#define CSV_WIDGET_SCENETOOLBAR_H + +#include + +class QVBoxLayout; + +namespace CSVWidget +{ + class SceneTool; + + class SceneToolbar : public QWidget + { + Q_OBJECT + + QVBoxLayout *mLayout; + int mButtonSize; + int mIconSize; + + protected: + + virtual void focusInEvent (QFocusEvent *event); + + public: + + SceneToolbar (int buttonSize, QWidget *parent = 0); + + /// If insertPoint==0, insert \a tool at the end of the scrollbar. Otherwise + /// insert tool after \a insertPoint. + void addTool (SceneTool *tool, SceneTool *insertPoint = 0); + + void removeTool (SceneTool *tool); + + int getButtonSize() const; + + int getIconSize() const; + + signals: + + void focusSceneRequest(); + }; +} + +#endif diff --git a/apps/opencs/view/widget/scenetoolmode.cpp b/apps/opencs/view/widget/scenetoolmode.cpp new file mode 100644 index 000000000..8d871cc5f --- /dev/null +++ b/apps/opencs/view/widget/scenetoolmode.cpp @@ -0,0 +1,103 @@ + +#include "scenetoolmode.hpp" + +#include +#include +#include + +#include "scenetoolbar.hpp" +#include "modebutton.hpp" + +void CSVWidget::SceneToolMode::adjustToolTip (const ModeButton *activeMode) +{ + QString toolTip = mToolTip; + + toolTip += "

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

(left click to change mode)"; + + setToolTip (toolTip); +} + +CSVWidget::SceneToolMode::SceneToolMode (SceneToolbar *parent, const QString& toolTip) +: SceneTool (parent), mButtonSize (parent->getButtonSize()), mIconSize (parent->getIconSize()), + mToolTip (toolTip), mFirst (0), mCurrent (0), mToolbar (parent) +{ + mPanel = new QFrame (this, Qt::Popup); + + mLayout = new QHBoxLayout (mPanel); + + mLayout->setContentsMargins (QMargins (0, 0, 0, 0)); + + mPanel->setLayout (mLayout); +} + +void CSVWidget::SceneToolMode::showPanel (const QPoint& position) +{ + mPanel->move (position); + mPanel->show(); + + if (mFirst) + mFirst->setFocus (Qt::OtherFocusReason); +} + +void CSVWidget::SceneToolMode::addButton (const std::string& icon, const std::string& id, + const QString& tooltip) +{ + ModeButton *button = new ModeButton (QIcon (QPixmap (icon.c_str())), tooltip, mPanel); + addButton (button, id); +} + +void CSVWidget::SceneToolMode::addButton (ModeButton *button, const std::string& id) +{ + button->setParent (mPanel); + + button->setSizePolicy (QSizePolicy (QSizePolicy::Fixed, QSizePolicy::Fixed)); + button->setIconSize (QSize (mIconSize, mIconSize)); + button->setFixedSize (mButtonSize, mButtonSize); + + mLayout->addWidget (button); + + mButtons.insert (std::make_pair (button, id)); + + connect (button, SIGNAL (clicked()), this, SLOT (selected())); + + if (mButtons.size()==1) + { + mFirst = mCurrent = button; + setIcon (button->icon()); + button->setChecked (true); + adjustToolTip (button); + mCurrent->activate (mToolbar); + } +} + +void CSVWidget::SceneToolMode::selected() +{ + std::map::const_iterator iter = + mButtons.find (dynamic_cast (sender())); + + if (iter!=mButtons.end()) + { + if (!iter->first->hasKeepOpen()) + mPanel->hide(); + + for (std::map::const_iterator iter2 = mButtons.begin(); + iter2!=mButtons.end(); ++iter2) + iter2->first->setChecked (iter2==iter); + + setIcon (iter->first->icon()); + adjustToolTip (iter->first); + + if (mCurrent!=iter->first) + { + if (mCurrent) + mCurrent->deactivate (mToolbar); + + mCurrent = iter->first; + mCurrent->activate (mToolbar); + } + + emit modeChanged (iter->second); + } +} \ No newline at end of file diff --git a/apps/opencs/view/widget/scenetoolmode.hpp b/apps/opencs/view/widget/scenetoolmode.hpp new file mode 100644 index 000000000..e6f462cf8 --- /dev/null +++ b/apps/opencs/view/widget/scenetoolmode.hpp @@ -0,0 +1,54 @@ +#ifndef CSV_WIDGET_SCENETOOL_MODE_H +#define CSV_WIDGET_SCENETOOL_MODE_H + +#include "scenetool.hpp" + +#include + +class QHBoxLayout; + +namespace CSVWidget +{ + class SceneToolbar; + class ModeButton; + + ///< \brief Mode selector tool + class SceneToolMode : public SceneTool + { + Q_OBJECT + + QWidget *mPanel; + QHBoxLayout *mLayout; + std::map mButtons; // widget, id + int mButtonSize; + int mIconSize; + QString mToolTip; + PushButton *mFirst; + ModeButton *mCurrent; + SceneToolbar *mToolbar; + + void adjustToolTip (const ModeButton *activeMode); + + public: + + SceneToolMode (SceneToolbar *parent, const QString& toolTip); + + virtual void showPanel (const QPoint& position); + + void addButton (const std::string& icon, const std::string& id, + const QString& tooltip = ""); + + /// The ownership of \a button is transferred to *this. + void addButton (ModeButton *button, const std::string& id); + + signals: + + void modeChanged (const std::string& id); + + private slots: + + void selected(); + }; +} + +#endif diff --git a/apps/opencs/view/widget/scenetoolrun.cpp b/apps/opencs/view/widget/scenetoolrun.cpp new file mode 100644 index 000000000..92f3193fe --- /dev/null +++ b/apps/opencs/view/widget/scenetoolrun.cpp @@ -0,0 +1,151 @@ + +#include "scenetoolrun.hpp" + +#include + +#include +#include +#include +#include +#include + +void CSVWidget::SceneToolRun::adjustToolTips() +{ + QString toolTip = mToolTip; + + if (mSelected==mProfiles.end()) + toolTip += "

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

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

(right click to switch to a different profile)"; + } + + setToolTip (toolTip); +} + +void CSVWidget::SceneToolRun::updateIcon() +{ + setIcon (QIcon (mSelected==mProfiles.end() ? mIconDisabled : mIcon)); +} + +void CSVWidget::SceneToolRun::updatePanel() +{ + mTable->setRowCount (mProfiles.size()); + + int i = 0; + + for (std::set::const_iterator iter (mProfiles.begin()); iter!=mProfiles.end(); + ++iter, ++i) + { + mTable->setItem (i, 0, new QTableWidgetItem (QString::fromUtf8 (iter->c_str()))); + + mTable->setItem (i, 1, new QTableWidgetItem ( + QApplication::style()->standardIcon (QStyle::SP_TitleBarCloseButton), "")); + } +} + +CSVWidget::SceneToolRun::SceneToolRun (SceneToolbar *parent, const QString& toolTip, + const QString& icon, const QString& iconDisabled, const std::vector& profiles) +: SceneTool (parent, Type_TopAction), mProfiles (profiles.begin(), profiles.end()), + mSelected (mProfiles.begin()), mToolTip (toolTip), mIcon (icon), + mIconDisabled (iconDisabled) +{ + updateIcon(); + adjustToolTips(); + + mPanel = new QFrame (this, Qt::Popup); + + QHBoxLayout *layout = new QHBoxLayout (mPanel); + + layout->setContentsMargins (QMargins (0, 0, 0, 0)); + + mTable = new QTableWidget (0, 2, this); + + mTable->setShowGrid (false); + mTable->verticalHeader()->hide(); + mTable->horizontalHeader()->hide(); + mTable->horizontalHeader()->setResizeMode (0, QHeaderView::Stretch); + mTable->horizontalHeader()->setResizeMode (1, QHeaderView::ResizeToContents); + mTable->setSelectionMode (QAbstractItemView::NoSelection); + + layout->addWidget (mTable); + + connect (mTable, SIGNAL (clicked (const QModelIndex&)), + this, SLOT (clicked (const QModelIndex&))); +} + +void CSVWidget::SceneToolRun::showPanel (const QPoint& position) +{ + updatePanel(); + + mPanel->move (position); + mPanel->show(); +} + +void CSVWidget::SceneToolRun::activate() +{ + if (mSelected!=mProfiles.end()) + emit runRequest (*mSelected); +} + +void CSVWidget::SceneToolRun::removeProfile (const std::string& profile) +{ + std::set::iterator iter = mProfiles.find (profile); + + if (iter!=mProfiles.end()) + { + if (iter==mSelected) + { + if (iter!=mProfiles.begin()) + --mSelected; + else + ++mSelected; + } + + mProfiles.erase (iter); + + if (mSelected==mProfiles.end()) + updateIcon(); + + adjustToolTips(); + } +} + +void CSVWidget::SceneToolRun::addProfile (const std::string& profile) +{ + std::set::iterator iter = mProfiles.find (profile); + + if (iter==mProfiles.end()) + { + mProfiles.insert (profile); + + if (mSelected==mProfiles.end()) + { + mSelected = mProfiles.begin(); + updateIcon(); + } + + adjustToolTips(); + } +} + +void CSVWidget::SceneToolRun::clicked (const QModelIndex& index) +{ + if (index.column()==0) + { + // select profile + mSelected = mProfiles.begin(); + std::advance (mSelected, index.row()); + mPanel->hide(); + adjustToolTips(); + } + else if (index.column()==1) + { + // remove profile from list + std::set::iterator iter = mProfiles.begin(); + std::advance (iter, index.row()); + removeProfile (*iter); + updatePanel(); + } +} \ No newline at end of file diff --git a/apps/opencs/view/widget/scenetoolrun.hpp b/apps/opencs/view/widget/scenetoolrun.hpp new file mode 100644 index 000000000..4396c2288 --- /dev/null +++ b/apps/opencs/view/widget/scenetoolrun.hpp @@ -0,0 +1,64 @@ +#ifndef CSV_WIDGET_SCENETOOLRUN_H +#define CSV_WIDGET_SCENETOOLRUN_H + +#include +#include + +#include "scenetool.hpp" + +class QFrame; +class QTableWidget; +class QModelIndex; + +namespace CSVWidget +{ + class SceneToolRun : public SceneTool + { + Q_OBJECT + + std::set mProfiles; + std::set::iterator mSelected; + QString mToolTip; + QString mIcon; + QString mIconDisabled; + QFrame *mPanel; + QTableWidget *mTable; + + private: + + void adjustToolTips(); + + void updateIcon(); + + void updatePanel(); + + public: + + SceneToolRun (SceneToolbar *parent, const QString& toolTip, const QString& icon, + const QString& iconDisabled, const std::vector& profiles); + + virtual void showPanel (const QPoint& position); + + virtual void activate(); + + /// \attention This function does not remove the profile from the profile selection + /// panel. + void removeProfile (const std::string& profile); + + /// \attention This function doe not add the profile to the profile selection + /// panel. This only happens when the panel is re-opened. + /// + /// \note Adding profiles that are already listed is a no-op. + void addProfile (const std::string& profile); + + private slots: + + void clicked (const QModelIndex& index); + + signals: + + void runRequest (const std::string& profile); + }; +} + +#endif diff --git a/apps/opencs/view/widget/scenetooltoggle.cpp b/apps/opencs/view/widget/scenetooltoggle.cpp new file mode 100644 index 000000000..07c448e45 --- /dev/null +++ b/apps/opencs/view/widget/scenetooltoggle.cpp @@ -0,0 +1,205 @@ + +#include "scenetooltoggle.hpp" + +#include + +#include +#include +#include +#include + +#include "scenetoolbar.hpp" +#include "pushbutton.hpp" + +void CSVWidget::SceneToolToggle::adjustToolTip() +{ + QString toolTip = mToolTip; + + toolTip += "

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

(left click to alter selection)"; + + setToolTip (toolTip); +} + +void CSVWidget::SceneToolToggle::adjustIcon() +{ + unsigned int selection = getSelection(); + if (!selection) + setIcon (QIcon (QString::fromUtf8 (mEmptyIcon.c_str()))); + else + { + QPixmap pixmap (48, 48); + pixmap.fill (QColor (0, 0, 0, 0)); + + { + QPainter painter (&pixmap); + + for (std::map::const_iterator iter (mButtons.begin()); + iter!=mButtons.end(); ++iter) + if (iter->first->isChecked()) + { + painter.drawImage (getIconBox (iter->second.mIndex), + QImage (QString::fromUtf8 (iter->second.mSmallIcon.c_str()))); + } + } + + setIcon (pixmap); + } +} + +QRect CSVWidget::SceneToolToggle::getIconBox (int index) const +{ + // layout for a 3x3 grid + int xMax = 3; + int yMax = 3; + + // icon size + int xBorder = 1; + int yBorder = 1; + + int iconXSize = (mIconSize-xBorder*(xMax+1))/xMax; + int iconYSize = (mIconSize-yBorder*(yMax+1))/yMax; + + int y = index / xMax; + int x = index % xMax; + + int total = mButtons.size(); + + int actualYIcons = total/xMax; + + if (total % xMax) + ++actualYIcons; + + if (actualYIcons!=yMax) + { + // space out icons vertically, if there aren't enough to populate all rows + int diff = yMax - actualYIcons; + yBorder += (diff*(yBorder+iconXSize)) / (actualYIcons+1); + } + + if (y==actualYIcons-1) + { + // generating the last row of icons + int actualXIcons = total % xMax; + + if (actualXIcons) + { + // space out icons horizontally, if there aren't enough to fill the last row + int diff = xMax - actualXIcons; + + xBorder += (diff*(xBorder+iconXSize)) / (actualXIcons+1); + } + } + + return QRect ((iconXSize+xBorder)*x+xBorder, (iconYSize+yBorder)*y+yBorder, + iconXSize, iconYSize); +} + +CSVWidget::SceneToolToggle::SceneToolToggle (SceneToolbar *parent, const QString& toolTip, + const std::string& emptyIcon) +: SceneTool (parent), mEmptyIcon (emptyIcon), mButtonSize (parent->getButtonSize()), + mIconSize (parent->getIconSize()), mToolTip (toolTip), mFirst (0) +{ + mPanel = new QFrame (this, Qt::Popup); + + mLayout = new QHBoxLayout (mPanel); + + mLayout->setContentsMargins (QMargins (0, 0, 0, 0)); + + mPanel->setLayout (mLayout); +} + +void CSVWidget::SceneToolToggle::showPanel (const QPoint& position) +{ + mPanel->move (position); + mPanel->show(); + + if (mFirst) + mFirst->setFocus (Qt::OtherFocusReason); +} + +void CSVWidget::SceneToolToggle::addButton (const std::string& icon, unsigned int id, + const std::string& smallIcon, const QString& name, const QString& tooltip) +{ + if (mButtons.size()>=9) + throw std::runtime_error ("Exceeded number of buttons in toggle type tool"); + + PushButton *button = new PushButton (QIcon (QPixmap (icon.c_str())), + PushButton::Type_Toggle, tooltip.isEmpty() ? name: tooltip, mPanel); + + button->setSizePolicy (QSizePolicy (QSizePolicy::Fixed, QSizePolicy::Fixed)); + button->setIconSize (QSize (mIconSize, mIconSize)); + button->setFixedSize (mButtonSize, mButtonSize); + + mLayout->addWidget (button); + + ButtonDesc desc; + desc.mId = id; + desc.mSmallIcon = smallIcon; + desc.mName = name; + desc.mIndex = mButtons.size(); + + mButtons.insert (std::make_pair (button, desc)); + + connect (button, SIGNAL (clicked()), this, SLOT (selected())); + + if (mButtons.size()==1) + mFirst = button; +} + +unsigned int CSVWidget::SceneToolToggle::getSelection() const +{ + unsigned int selection = 0; + + for (std::map::const_iterator iter (mButtons.begin()); + iter!=mButtons.end(); ++iter) + if (iter->first->isChecked()) + selection |= iter->second.mId; + + return selection; +} + +void CSVWidget::SceneToolToggle::setSelection (unsigned int selection) +{ + for (std::map::iterator iter (mButtons.begin()); + iter!=mButtons.end(); ++iter) + iter->first->setChecked (selection & iter->second.mId); + + adjustToolTip(); + adjustIcon(); +} + +void CSVWidget::SceneToolToggle::selected() +{ + std::map::const_iterator iter = + mButtons.find (dynamic_cast (sender())); + + if (iter!=mButtons.end()) + { + if (!iter->first->hasKeepOpen()) + mPanel->hide(); + + adjustToolTip(); + adjustIcon(); + + emit selectionChanged(); + } +} diff --git a/apps/opencs/view/widget/scenetooltoggle.hpp b/apps/opencs/view/widget/scenetooltoggle.hpp new file mode 100644 index 000000000..55e697524 --- /dev/null +++ b/apps/opencs/view/widget/scenetooltoggle.hpp @@ -0,0 +1,75 @@ +#ifndef CSV_WIDGET_SCENETOOL_TOGGLE_H +#define CSV_WIDGET_SCENETOOL_TOGGLE_H + +#include "scenetool.hpp" + +#include + +class QHBoxLayout; +class QRect; + +namespace CSVWidget +{ + class SceneToolbar; + class PushButton; + + ///< \brief Multi-Toggle tool + class SceneToolToggle : public SceneTool + { + Q_OBJECT + + struct ButtonDesc + { + unsigned int mId; + std::string mSmallIcon; + QString mName; + int mIndex; + }; + + std::string mEmptyIcon; + QWidget *mPanel; + QHBoxLayout *mLayout; + std::map mButtons; // widget, id + int mButtonSize; + int mIconSize; + QString mToolTip; + PushButton *mFirst; + + void adjustToolTip(); + + void adjustIcon(); + + QRect getIconBox (int index) const; + + public: + + SceneToolToggle (SceneToolbar *parent, const QString& toolTip, + const std::string& emptyIcon); + + virtual void showPanel (const QPoint& position); + + /// \attention After the last button has been added, setSelection must be called at + /// least once to finalise the layout. + /// + /// \note The layout algorithm can not handle more than 9 buttons. To prevent this An + /// attempt to add more will result in an exception being thrown. + /// The small icons will be sized at (x-4)/3 (where x is the main icon size). + void addButton (const std::string& icon, unsigned int id, + const std::string& smallIcon, const QString& name, const QString& tooltip = ""); + + unsigned int getSelection() const; + + /// \param or'ed button IDs. IDs that do not exist will be ignored. + void setSelection (unsigned int selection); + + signals: + + void selectionChanged(); + + private slots: + + void selected(); + }; +} + +#endif diff --git a/apps/opencs/view/world/creator.cpp b/apps/opencs/view/world/creator.cpp index d753a2b47..a24c58e54 100644 --- a/apps/opencs/view/world/creator.cpp +++ b/apps/opencs/view/world/creator.cpp @@ -1,7 +1,16 @@ #include "creator.hpp" -CSVWorld::Creator:: ~Creator() {} +#include + +CSVWorld::Creator::~Creator() {} + +void CSVWorld::Creator::setScope (unsigned int scope) +{ + if (scope!=CSMWorld::Scope_Content) + throw std::logic_error ("Invalid scope in creator"); +} + CSVWorld::CreatorFactoryBase::~CreatorFactoryBase() {} diff --git a/apps/opencs/view/world/creator.hpp b/apps/opencs/view/world/creator.hpp index 88da70330..8e50e8715 100644 --- a/apps/opencs/view/world/creator.hpp +++ b/apps/opencs/view/world/creator.hpp @@ -1,9 +1,14 @@ #ifndef CSV_WORLD_CREATOR_H #define CSV_WORLD_CREATOR_H +#include + #include + #include "../../model/world/universalid.hpp" +#include "../../model/world/scope.hpp" + class QUndoStack; namespace CSMWorld @@ -32,6 +37,9 @@ namespace CSVWorld virtual void toggleWidgets(bool active = true) = 0; + /// Default implementation: Throw an exception if scope!=Scope_Content. + virtual void setScope (unsigned int scope); + signals: void done(); @@ -68,7 +76,7 @@ namespace CSVWorld /// \note The function always returns 0. }; - template + template class CreatorFactory : public CreatorFactoryBase { public: @@ -81,11 +89,15 @@ namespace CSVWorld /// records should be provided. }; - template - Creator *CreatorFactory::makeCreator (CSMWorld::Data& data, QUndoStack& undoStack, + template + Creator *CreatorFactory::makeCreator (CSMWorld::Data& data, QUndoStack& undoStack, const CSMWorld::UniversalId& id) const { - return new CreatorT (data, undoStack, id); + std::auto_ptr creator (new CreatorT (data, undoStack, id)); + + creator->setScope (scope); + + return creator.release(); } } diff --git a/apps/opencs/view/world/datadisplaydelegate.cpp b/apps/opencs/view/world/datadisplaydelegate.cpp index 31ec18d52..46ca17a29 100644 --- a/apps/opencs/view/world/datadisplaydelegate.cpp +++ b/apps/opencs/view/world/datadisplaydelegate.cpp @@ -6,11 +6,11 @@ CSVWorld::DataDisplayDelegate::DataDisplayDelegate(const ValueList &values, const IconList &icons, - QUndoStack &undoStack, + CSMDoc::Document& document, const QString &pageName, const QString &settingName, QObject *parent) - : EnumDelegate (values, undoStack, parent), mDisplayMode (Mode_TextOnly), + : EnumDelegate (values, document, parent), mDisplayMode (Mode_TextOnly), mIcons (icons), mIconSize (QSize(16, 16)), mIconLeftOffset(3), mTextLeftOffset(8), mSettingKey (pageName + '/' + settingName) { @@ -38,7 +38,7 @@ void CSVWorld::DataDisplayDelegate::buildPixmaps () } } -void CSVWorld::DataDisplayDelegate::setIconSize(const QSize size) +void CSVWorld::DataDisplayDelegate::setIconSize(const QSize& size) { mIconSize = size; buildPixmaps(); @@ -126,8 +126,6 @@ void CSVWorld::DataDisplayDelegate::updateDisplayMode (const QString &mode) CSVWorld::DataDisplayDelegate::~DataDisplayDelegate() { - mIcons.clear(); - mPixmaps.clear(); } void CSVWorld::DataDisplayDelegateFactory::add (int enumValue, QString enumName, QString iconFilename) @@ -137,11 +135,10 @@ void CSVWorld::DataDisplayDelegateFactory::add (int enumValue, QString enumName, } -CSVWorld::CommandDelegate *CSVWorld::DataDisplayDelegateFactory::makeDelegate (QUndoStack& undoStack, - QObject *parent) const +CSVWorld::CommandDelegate *CSVWorld::DataDisplayDelegateFactory::makeDelegate ( + CSMDoc::Document& document, QObject *parent) const { - - return new DataDisplayDelegate (mValues, mIcons, undoStack, "", "", parent); + return new DataDisplayDelegate (mValues, mIcons, document, "", "", parent); } diff --git a/apps/opencs/view/world/datadisplaydelegate.hpp b/apps/opencs/view/world/datadisplaydelegate.hpp index ef453c58f..73790e3c6 100755 --- a/apps/opencs/view/world/datadisplaydelegate.hpp +++ b/apps/opencs/view/world/datadisplaydelegate.hpp @@ -40,7 +40,7 @@ namespace CSVWorld public: explicit DataDisplayDelegate (const ValueList & values, const IconList & icons, - QUndoStack& undoStack, + CSMDoc::Document& document, const QString &pageName, const QString &settingName, QObject *parent); @@ -50,7 +50,7 @@ namespace CSVWorld virtual void paint (QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const; /// pass a QSize defining height / width of icon. Default is QSize (16,16). - void setIconSize (const QSize icon); + void setIconSize (const QSize& icon); /// offset the horizontal position of the icon from the left edge of the cell. Default is 3 pixels. void setIconLeftOffset (int offset); @@ -82,7 +82,7 @@ namespace CSVWorld public: - virtual CommandDelegate *makeDelegate (QUndoStack& undoStack, QObject *parent) const; + virtual CommandDelegate *makeDelegate (CSMDoc::Document& document, QObject *parent) const; ///< The ownership of the returned CommandDelegate is transferred to the caller. protected: diff --git a/apps/opencs/view/world/dialoguesubview.cpp b/apps/opencs/view/world/dialoguesubview.cpp index d03bf3f80..8790601ea 100644 --- a/apps/opencs/view/world/dialoguesubview.cpp +++ b/apps/opencs/view/world/dialoguesubview.cpp @@ -167,10 +167,10 @@ void CSVWorld::DialogueDelegateDispatcherProxy::tableMimeDataDropped(const std:: ==============================DialogueDelegateDispatcher========================================== */ -CSVWorld::DialogueDelegateDispatcher::DialogueDelegateDispatcher(QObject* parent, CSMWorld::IdTable* table, QUndoStack& undoStack) : +CSVWorld::DialogueDelegateDispatcher::DialogueDelegateDispatcher(QObject* parent, CSMWorld::IdTable* table, CSMDoc::Document& document) : mParent(parent), mTable(table), -mUndoStack(undoStack), +mDocument (document), mNotEditableDelegate(table, parent) { } @@ -182,7 +182,7 @@ CSVWorld::CommandDelegate* CSVWorld::DialogueDelegateDispatcher::makeDelegate(CS if (delegateIt == mDelegates.end()) { delegate = CommandDelegateFactoryCollection::get().makeDelegate ( - display, mUndoStack, mParent); + display, mDocument, mParent); mDelegates.insert(std::make_pair(display, delegate)); } else { @@ -266,7 +266,6 @@ QWidget* CSVWorld::DialogueDelegateDispatcher::makeEditor(CSMWorld::ColumnBase:: editor = delegateIt->second->createEditor(qobject_cast(mParent), QStyleOptionViewItem(), index, display); DialogueDelegateDispatcherProxy* proxy = new DialogueDelegateDispatcherProxy(editor, display); - bool skip = false; if (qobject_cast(editor)) { connect(editor, SIGNAL(editingFinished()), proxy, SLOT(editorDataCommited())); @@ -274,27 +273,22 @@ QWidget* CSVWorld::DialogueDelegateDispatcher::makeEditor(CSMWorld::ColumnBase:: proxy, SLOT(tableMimeDataDropped(const std::vector&, const CSMDoc::Document*))); connect(proxy, SIGNAL(tableMimeDataDropped(QWidget*, const QModelIndex&, const CSMWorld::UniversalId&, const CSMDoc::Document*)), this, SIGNAL(tableMimeDataDropped(QWidget*, const QModelIndex&, const CSMWorld::UniversalId&, const CSMDoc::Document*))); - skip = true; } - if(!skip && qobject_cast(editor)) + else if (qobject_cast(editor)) { connect(editor, SIGNAL(stateChanged(int)), proxy, SLOT(editorDataCommited())); - skip = true; } - if(!skip && qobject_cast(editor)) + else if (qobject_cast(editor)) { connect(editor, SIGNAL(textChanged()), proxy, SLOT(editorDataCommited())); - skip = true; } - if(!skip && qobject_cast(editor)) + else if (qobject_cast(editor)) { connect(editor, SIGNAL(currentIndexChanged (int)), proxy, SLOT(editorDataCommited())); - skip = true; } - if(!skip && qobject_cast(editor)) + else if (qobject_cast(editor)) { connect(editor, SIGNAL(editingFinished()), proxy, SLOT(editorDataCommited())); - skip = true; } connect(proxy, SIGNAL(editorDataCommited(QWidget*, const QModelIndex&, CSMWorld::ColumnBase::Display)), this, SLOT(editorDataCommited(QWidget*, const QModelIndex&, CSMWorld::ColumnBase::Display))); @@ -315,12 +309,12 @@ CSVWorld::DialogueDelegateDispatcher::~DialogueDelegateDispatcher() =============================================================EditWidget===================================================== */ -CSVWorld::EditWidget::EditWidget(QWidget *parent, int row, CSMWorld::IdTable* table, QUndoStack& undoStack, bool createAndDelete) : -mDispatcher(this, table, undoStack), +CSVWorld::EditWidget::EditWidget(QWidget *parent, int row, CSMWorld::IdTable* table, CSMDoc::Document& document, bool createAndDelete) : +mDispatcher(this, table, document), QScrollArea(parent), mWidgetMapper(NULL), mMainWidget(NULL), -mUndoStack(undoStack), +mDocument (document), mTable(table) { remake (row); @@ -332,6 +326,7 @@ void CSVWorld::EditWidget::remake(int row) if (mMainWidget) { delete mMainWidget; + mMainWidget = 0; } mMainWidget = new QWidget (this); @@ -339,6 +334,7 @@ void CSVWorld::EditWidget::remake(int row) if (mWidgetMapper) { delete mWidgetMapper; + mWidgetMapper = 0; } mWidgetMapper = new QDataWidgetMapper (this); mWidgetMapper->setModel(mTable); @@ -396,7 +392,6 @@ void CSVWorld::EditWidget::remake(int row) mWidgetMapper->setCurrentModelIndex(mTable->index(row, 0)); - this->setMinimumWidth(325); //TODO find better way to set the width or make it customizable this->setWidget(mMainWidget); this->setWidgetResizable(true); } @@ -407,7 +402,6 @@ void CSVWorld::EditWidget::remake(int row) CSVWorld::DialogueSubView::DialogueSubView (const CSMWorld::UniversalId& id, CSMDoc::Document& document, const CreatorFactoryBase& creatorFactory, bool sorting) : - SubView (id), mEditWidget(0), mMainLayout(NULL), @@ -415,8 +409,8 @@ CSVWorld::DialogueSubView::DialogueSubView (const CSMWorld::UniversalId& id, CSM mTable(dynamic_cast(document.getData().getTableModel(id))), mRow (-1), mLocked(false), - mDocument(document) - + mDocument(document), + mCommandDispatcher (document, CSMWorld::UniversalId::getParentType (id.getType())) { connect(mTable, SIGNAL(dataChanged (const QModelIndex&, const QModelIndex&)), this, SLOT(dataChanged(const QModelIndex&))); mRow = mTable->getModelIndex (id.getId(), 0).row(); @@ -425,33 +419,41 @@ CSVWorld::DialogueSubView::DialogueSubView (const CSMWorld::UniversalId& id, CSM QHBoxLayout *buttonsLayout = new QHBoxLayout; QToolButton* prevButton = new QToolButton(mainWidget); prevButton->setIcon(QIcon(":/go-previous.png")); + prevButton->setToolTip ("Switch to previous record"); QToolButton* nextButton = new QToolButton(mainWidget); nextButton->setIcon(QIcon(":/go-next.png")); + nextButton->setToolTip ("Switch to next record"); buttonsLayout->addWidget(prevButton, 0); buttonsLayout->addWidget(nextButton, 1); buttonsLayout->addStretch(2); QToolButton* cloneButton = new QToolButton(mainWidget); cloneButton->setIcon(QIcon(":/edit-clone.png")); + cloneButton->setToolTip ("Clone record"); QToolButton* addButton = new QToolButton(mainWidget); addButton->setIcon(QIcon(":/add.png")); + addButton->setToolTip ("Add new record"); QToolButton* deleteButton = new QToolButton(mainWidget); deleteButton->setIcon(QIcon(":/edit-delete.png")); + deleteButton->setToolTip ("Delete record"); QToolButton* revertButton = new QToolButton(mainWidget); revertButton->setIcon(QIcon(":/edit-undo.png")); + revertButton->setToolTip ("Revert record"); - if (mTable->hasPreview()) + if (mTable->getFeatures() & CSMWorld::IdTable::Feature_Preview) { QToolButton* previewButton = new QToolButton(mainWidget); previewButton->setIcon(QIcon(":/edit-preview.png")); + previewButton->setToolTip ("Open a preview of this record"); buttonsLayout->addWidget(previewButton); connect(previewButton, SIGNAL(clicked()), this, SLOT(showPreview())); } - if (mTable->getViewing()!=CSMWorld::IdTable::Viewing_None) + if (mTable->getFeatures() & CSMWorld::IdTable::Feature_View) { QToolButton* viewButton = new QToolButton(mainWidget); viewButton->setIcon(QIcon(":/cell.png")); + viewButton->setToolTip ("Open a scene view of the cell this record is located in"); buttonsLayout->addWidget(viewButton); connect(viewButton, SIGNAL(clicked()), this, SLOT(viewRecord())); } @@ -469,7 +471,7 @@ CSVWorld::DialogueSubView::DialogueSubView (const CSMWorld::UniversalId& id, CSM mMainLayout = new QVBoxLayout(mainWidget); - mEditWidget = new EditWidget(mainWidget, mRow, mTable, mUndoStack, false); + mEditWidget = new EditWidget(mainWidget, mRow, mTable, document, false); connect(mEditWidget, SIGNAL(tableMimeDataDropped(QWidget*, const QModelIndex&, const CSMWorld::UniversalId&, const CSMDoc::Document*)), this, SLOT(tableMimeDataDropped(QWidget*, const QModelIndex&, const CSMWorld::UniversalId&, const CSMDoc::Document*))); @@ -560,14 +562,12 @@ void CSVWorld::DialogueSubView::nextId() void CSVWorld::DialogueSubView::setEditLock (bool locked) { mLocked = locked; + CSMWorld::RecordBase::State state = static_cast(mTable->data (mTable->index (mRow, 1)).toInt()); - if (state == CSMWorld::RecordBase::State_Deleted || state == CSMWorld::RecordBase::State_Erased) - { - mEditWidget->setDisabled(true); - } else - { - mEditWidget->setDisabled(mLocked); - } + + mEditWidget->setDisabled (state==CSMWorld::RecordBase::State_Deleted || locked); + + mCommandDispatcher.setEditLock (locked); } void CSVWorld::DialogueSubView::dataChanged(const QModelIndex & index) @@ -575,13 +575,8 @@ void CSVWorld::DialogueSubView::dataChanged(const QModelIndex & index) if (index.row() == mRow) { CSMWorld::RecordBase::State state = static_cast(mTable->data (mTable->index (mRow, 1)).toInt()); - if (state == CSMWorld::RecordBase::State_Deleted || state == CSMWorld::RecordBase::State_Erased) - { - mEditWidget->setDisabled(true); - } else - { - mEditWidget->setDisabled(mLocked); - } + + mEditWidget->setDisabled (state==CSMWorld::RecordBase::State_Deleted || mLocked); } } @@ -671,7 +666,8 @@ void CSVWorld::DialogueSubView::cloneRequest () void CSVWorld::DialogueSubView::showPreview () { - if (mTable->hasPreview() && mRow < mTable->rowCount()) + if ((mTable->getFeatures() & CSMWorld::IdTable::Feature_Preview) && + mRow < mTable->rowCount()) { emit focusId(CSMWorld::UniversalId(CSMWorld::UniversalId::Type_Preview, mTable->data(mTable->index (mRow, 0)).toString().toUtf8().constData()), ""); } diff --git a/apps/opencs/view/world/dialoguesubview.hpp b/apps/opencs/view/world/dialoguesubview.hpp index 5642f46a0..4c260170f 100644 --- a/apps/opencs/view/world/dialoguesubview.hpp +++ b/apps/opencs/view/world/dialoguesubview.hpp @@ -8,7 +8,9 @@ #include #include "../doc/subview.hpp" + #include "../../model/world/columnbase.hpp" +#include "../../model/world/commanddispatcher.hpp" class QDataWidgetMapper; class QSize; @@ -99,14 +101,14 @@ namespace CSVWorld CSMWorld::IdTable* mTable; - QUndoStack& mUndoStack; + CSMDoc::Document& mDocument; NotEditableSubDelegate mNotEditableDelegate; std::vector mProxys; //once we move to the C++11 we should use unique_ptr public: - DialogueDelegateDispatcher(QObject* parent, CSMWorld::IdTable* table, QUndoStack& undoStack); + DialogueDelegateDispatcher(QObject* parent, CSMWorld::IdTable* table, CSMDoc::Document& document); ~DialogueDelegateDispatcher(); @@ -143,11 +145,11 @@ namespace CSVWorld DialogueDelegateDispatcher mDispatcher; QWidget* mMainWidget; CSMWorld::IdTable* mTable; - QUndoStack& mUndoStack; + CSMDoc::Document& mDocument; public: - EditWidget (QWidget *parent, int row, CSMWorld::IdTable* table, QUndoStack& undoStack, bool createAndDelete = false); + EditWidget (QWidget *parent, int row, CSMWorld::IdTable* table, CSMDoc::Document& document, bool createAndDelete = false); void remake(int row); @@ -169,6 +171,7 @@ namespace CSVWorld bool mLocked; const CSMDoc::Document& mDocument; TableBottomBox* mBottom; + CSMWorld::CommandDispatcher mCommandDispatcher; public: diff --git a/apps/opencs/view/world/enumdelegate.cpp b/apps/opencs/view/world/enumdelegate.cpp index 377f479bf..168e5cb0a 100644 --- a/apps/opencs/view/world/enumdelegate.cpp +++ b/apps/opencs/view/world/enumdelegate.cpp @@ -35,8 +35,8 @@ void CSVWorld::EnumDelegate::addCommands (QAbstractItemModel *model, CSVWorld::EnumDelegate::EnumDelegate (const std::vector >& values, - QUndoStack& undoStack, QObject *parent) -: CommandDelegate (undoStack, parent), mValues (values) + CSMDoc::Document& document, QObject *parent) +: CommandDelegate (document, parent), mValues (values) { } @@ -140,10 +140,10 @@ CSVWorld::EnumDelegateFactory::EnumDelegateFactory (const std::vector >& values, - QUndoStack& undoStack, QObject *parent); + CSMDoc::Document& document, QObject *parent); virtual QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem& option, @@ -64,7 +64,7 @@ namespace CSVWorld EnumDelegateFactory (const std::vector& names, bool allowNone = false); /// \param allowNone Use value of -1 for "none selected" (empty string) - virtual CommandDelegate *makeDelegate (QUndoStack& undoStack, QObject *parent) const; + virtual CommandDelegate *makeDelegate (CSMDoc::Document& document, QObject *parent) const; ///< The ownership of the returned CommandDelegate is transferred to the caller. void add (int value, const QString& name); diff --git a/apps/opencs/view/world/genericcreator.cpp b/apps/opencs/view/world/genericcreator.cpp index 31c216e2c..afa59bc45 100644 --- a/apps/opencs/view/world/genericcreator.cpp +++ b/apps/opencs/view/world/genericcreator.cpp @@ -7,6 +7,10 @@ #include #include #include +#include +#include + +#include #include "../../model/world/commands.hpp" #include "../../model/world/data.hpp" @@ -46,32 +50,94 @@ std::string CSVWorld::GenericCreator::getId() const void CSVWorld::GenericCreator::configureCreateCommand (CSMWorld::CreateCommand& command) const {} +void CSVWorld::GenericCreator::pushCommand (std::auto_ptr command, + const std::string& id) +{ + mUndoStack.push (command.release()); +} + CSMWorld::Data& CSVWorld::GenericCreator::getData() const { return mData; } +QUndoStack& CSVWorld::GenericCreator::getUndoStack() +{ + return mUndoStack; +} + const CSMWorld::UniversalId& CSVWorld::GenericCreator::getCollectionId() const { return mListId; } -CSVWorld::GenericCreator::GenericCreator (CSMWorld::Data& data, QUndoStack& undoStack, - const CSMWorld::UniversalId& id, bool relaxedIdRules): +std::string CSVWorld::GenericCreator::getNamespace() const +{ + CSMWorld::Scope scope = CSMWorld::Scope_Content; - mData (data), - mUndoStack (undoStack), - mListId (id), - mLocked (false), - mCloneMode(false), - mClonedType(CSMWorld::UniversalId::Type_None) + if (mScope) + { + scope = static_cast (mScope->itemData (mScope->currentIndex()).toInt()); + } + else + { + if (mScopes & CSMWorld::Scope_Project) + scope = CSMWorld::Scope_Project; + else if (mScopes & CSMWorld::Scope_Session) + scope = CSMWorld::Scope_Session; + } + switch (scope) + { + case CSMWorld::Scope_Content: return ""; + case CSMWorld::Scope_Project: return "project::"; + case CSMWorld::Scope_Session: return "session::"; + } + + return ""; +} + +void CSVWorld::GenericCreator::updateNamespace() +{ + std::string namespace_ = getNamespace(); + + mValidator->setNamespace (namespace_); + + int index = mId->text().indexOf ("::"); + + if (index==-1) + { + // no namespace in old text + mId->setText (QString::fromUtf8 (namespace_.c_str()) + mId->text()); + } + else + { + std::string oldNamespace = + Misc::StringUtils::lowerCase (mId->text().left (index).toUtf8().constData()); + + if (oldNamespace=="project" || oldNamespace=="session") + mId->setText (QString::fromUtf8 (namespace_.c_str()) + mId->text().mid (index+2)); + } +} + +void CSVWorld::GenericCreator::addScope (const QString& name, CSMWorld::Scope scope, + const QString& tooltip) +{ + mScope->addItem (name, static_cast (scope)); + mScope->setItemData (mScope->count()-1, tooltip, Qt::ToolTipRole); +} + +CSVWorld::GenericCreator::GenericCreator (CSMWorld::Data& data, QUndoStack& undoStack, + const CSMWorld::UniversalId& id, bool relaxedIdRules) +: mData (data), mUndoStack (undoStack), mListId (id), mLocked (false), mCloneMode (false), + mClonedType (CSMWorld::UniversalId::Type_None), mScopes (CSMWorld::Scope_Content), mScope (0), + mScopeLabel (0) { mLayout = new QHBoxLayout; mLayout->setContentsMargins (0, 0, 0, 0); mId = new QLineEdit; - mId->setValidator (new IdValidator (relaxedIdRules, this)); + mId->setValidator (mValidator = new IdValidator (relaxedIdRules, this)); mLayout->addWidget (mId, 1); mCreate = new QPushButton ("Create"); @@ -99,22 +165,17 @@ void CSVWorld::GenericCreator::reset() mCloneMode = false; mId->setText (""); update(); + updateNamespace(); } std::string CSVWorld::GenericCreator::getErrors() const { std::string errors; - std::string id = getId(); - - if (id.empty()) - { - errors = "Missing ID"; - } - else if (mData.hasId (id)) - { + if (!mId->hasAcceptableInput()) + errors = mValidator->getError(); + else if (mData.hasId (getId())) errors = "ID is already in use"; - } return errors; } @@ -128,29 +189,27 @@ void CSVWorld::GenericCreator::create() { if (!mLocked) { + std::string id = getId(); + + std::auto_ptr command; + if (mCloneMode) { - std::string id = getId(); - std::auto_ptr command (new CSMWorld::CloneCommand ( + command.reset (new CSMWorld::CloneCommand ( dynamic_cast (*mData.getTableModel(mListId)), mClonedId, id, mClonedType)); + } + else + { + command.reset (new CSMWorld::CreateCommand ( + dynamic_cast (*mData.getTableModel (mListId)), id)); - mUndoStack.push(command.release()); - - emit done(); - emit requestFocus(id); - } else { - std::string id = getId(); - - std::auto_ptr command (new CSMWorld::CreateCommand ( - dynamic_cast (*mData.getTableModel (mListId)), id)); - - configureCreateCommand (*command); + } - mUndoStack.push (command.release()); + configureCreateCommand (*command); + pushCommand (command, id); - emit done(); - emit requestFocus (id); - } + emit done(); + emit requestFocus(id); } } @@ -165,3 +224,56 @@ void CSVWorld::GenericCreator::cloneMode(const std::string& originId, void CSVWorld::GenericCreator::toggleWidgets(bool active) { } + +void CSVWorld::GenericCreator::setScope (unsigned int scope) +{ + mScopes = scope; + int count = (mScopes & CSMWorld::Scope_Content) + (mScopes & CSMWorld::Scope_Project) + + (mScopes & CSMWorld::Scope_Session); + + // scope selector widget + if (count>1) + { + mScope = new QComboBox (this); + insertAtBeginning (mScope, false); + + if (mScopes & CSMWorld::Scope_Content) + addScope ("Content", CSMWorld::Scope_Content, + "Record will be stored in the currently edited content file."); + + if (mScopes & CSMWorld::Scope_Project) + addScope ("Project", CSMWorld::Scope_Project, + "Record will be stored in a local project file.

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

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

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

" + "Record is not available when running OpenMW via OpenCS."); + + connect (mScope, SIGNAL (currentIndexChanged (int)), this, SLOT (scopeChanged (int))); + + mScopeLabel = new QLabel ("Scope", this); + insertAtBeginning (mScopeLabel, false); + + mScope->setCurrentIndex (0); + } + else + { + delete mScope; + mScope = 0; + + delete mScopeLabel; + mScopeLabel = 0; + } + + updateNamespace(); +} + +void CSVWorld::GenericCreator::scopeChanged (int index) +{ + update(); + updateNamespace(); +} \ No newline at end of file diff --git a/apps/opencs/view/world/genericcreator.hpp b/apps/opencs/view/world/genericcreator.hpp index 714853f98..8c8c34bd8 100644 --- a/apps/opencs/view/world/genericcreator.hpp +++ b/apps/opencs/view/world/genericcreator.hpp @@ -1,14 +1,18 @@ #ifndef CSV_WORLD_GENERICCREATOR_H #define CSV_WORLD_GENERICCREATOR_H +#include + +#include "../../model/world/universalid.hpp" + +#include "creator.hpp" + class QString; class QPushButton; class QLineEdit; class QHBoxLayout; - -#include "creator.hpp" - -#include "../../model/world/universalid.hpp" +class QComboBox; +class QLabel; namespace CSMWorld { @@ -17,6 +21,8 @@ namespace CSMWorld namespace CSVWorld { + class IdValidator; + class GenericCreator : public Creator { Q_OBJECT @@ -31,6 +37,10 @@ namespace CSVWorld bool mLocked; std::string mClonedId; CSMWorld::UniversalId::Type mClonedType; + unsigned int mScopes; + QComboBox *mScope; + QLabel *mScopeLabel; + IdValidator *mValidator; protected: bool mCloneMode; @@ -48,12 +58,29 @@ namespace CSVWorld virtual std::string getId() const; + /// Allow subclasses to add additional data to \a command. virtual void configureCreateCommand (CSMWorld::CreateCommand& command) const; + /// Allow subclasses to wrap the create command together with additional commands + /// into a macro. + virtual void pushCommand (std::auto_ptr command, + const std::string& id); + CSMWorld::Data& getData() const; + QUndoStack& getUndoStack(); + const CSMWorld::UniversalId& getCollectionId() const; + std::string getNamespace() const; + + private: + + void updateNamespace(); + + void addScope (const QString& name, CSMWorld::Scope scope, + const QString& tooltip); + public: GenericCreator (CSMWorld::Data& data, QUndoStack& undoStack, @@ -65,18 +92,22 @@ namespace CSVWorld virtual void toggleWidgets (bool active = true); - virtual void cloneMode(const std::string& originId, + virtual void cloneMode(const std::string& originId, const CSMWorld::UniversalId::Type type); virtual std::string getErrors() const; ///< Return formatted error descriptions for the current state of the creator. if an empty /// string is returned, there is no error. + virtual void setScope (unsigned int scope); + private slots: void textChanged (const QString& text); void create(); + + void scopeChanged (int index); }; } diff --git a/apps/opencs/view/world/idtypedelegate.cpp b/apps/opencs/view/world/idtypedelegate.cpp index 6b4d442f3..3b440ff71 100755 --- a/apps/opencs/view/world/idtypedelegate.cpp +++ b/apps/opencs/view/world/idtypedelegate.cpp @@ -3,9 +3,9 @@ #include "../../model/world/universalid.hpp" CSVWorld::IdTypeDelegate::IdTypeDelegate - (const ValueList &values, const IconList &icons, QUndoStack& undoStack, QObject *parent) - : DataDisplayDelegate (values, icons, undoStack, - "Display Format", "Referenceable ID Type Display", + (const ValueList &values, const IconList &icons, CSMDoc::Document& document, QObject *parent) + : DataDisplayDelegate (values, icons, document, + "records", "type-format", parent) {} @@ -20,8 +20,8 @@ CSVWorld::IdTypeDelegateFactory::IdTypeDelegateFactory() } } -CSVWorld::CommandDelegate *CSVWorld::IdTypeDelegateFactory::makeDelegate (QUndoStack& undoStack, - QObject *parent) const +CSVWorld::CommandDelegate *CSVWorld::IdTypeDelegateFactory::makeDelegate ( + CSMDoc::Document& document, QObject *parent) const { - return new IdTypeDelegate (mValues, mIcons, undoStack, parent); + return new IdTypeDelegate (mValues, mIcons, document, parent); } diff --git a/apps/opencs/view/world/idtypedelegate.hpp b/apps/opencs/view/world/idtypedelegate.hpp index bed81f2d5..e9a0af68c 100755 --- a/apps/opencs/view/world/idtypedelegate.hpp +++ b/apps/opencs/view/world/idtypedelegate.hpp @@ -11,7 +11,7 @@ namespace CSVWorld class IdTypeDelegate : public DataDisplayDelegate { public: - IdTypeDelegate (const ValueList &mValues, const IconList &icons, QUndoStack& undoStack, QObject *parent); + IdTypeDelegate (const ValueList &mValues, const IconList &icons, CSMDoc::Document& document, QObject *parent); }; class IdTypeDelegateFactory : public DataDisplayDelegateFactory @@ -20,7 +20,7 @@ namespace CSVWorld IdTypeDelegateFactory(); - virtual CommandDelegate *makeDelegate (QUndoStack& undoStack, QObject *parent) const; + virtual CommandDelegate *makeDelegate (CSMDoc::Document& document, QObject *parent) const; ///< The ownership of the returned CommandDelegate is transferred to the caller. }; } diff --git a/apps/opencs/view/world/idvalidator.cpp b/apps/opencs/view/world/idvalidator.cpp index 7c210daae..7caa20f9b 100644 --- a/apps/opencs/view/world/idvalidator.cpp +++ b/apps/opencs/view/world/idvalidator.cpp @@ -1,6 +1,8 @@ #include "idvalidator.hpp" +#include + bool CSVWorld::IdValidator::isValid (const QChar& c, bool first) const { if (c.isLetter() || c=='_') @@ -18,6 +20,8 @@ CSVWorld::IdValidator::IdValidator (bool relaxed, QObject *parent) QValidator::State CSVWorld::IdValidator::validate (QString& input, int& pos) const { + mError.clear(); + if (mRelaxed) { if (input.indexOf ('"')!=-1 || input.indexOf ("::")!=-1 || input.indexOf ("#")!=-1) @@ -25,12 +29,95 @@ QValidator::State CSVWorld::IdValidator::validate (QString& input, int& pos) con } else { + if (input.isEmpty()) + { + mError = "Missing ID"; + return QValidator::Intermediate; + } + bool first = true; + bool scope = false; + bool prevScope = false; + + QString::const_iterator iter = input.begin(); + + if (!mNamespace.empty()) + { + std::string namespace_ = input.left (mNamespace.size()).toUtf8().constData(); + + if (Misc::StringUtils::lowerCase (namespace_)!=mNamespace) + return QValidator::Invalid; // incorrect namespace + + iter += namespace_.size(); + first = false; + prevScope = true; + } + else + { + int index = input.indexOf (":"); + + if (index!=-1) + { + QString namespace_ = input.left (index); + + if (namespace_=="project" || namespace_=="session") + return QValidator::Invalid; // reserved namespace + } + } + + for (; iter!=input.end(); ++iter, first = false) + { + if (*iter==':') + { + if (first) + return QValidator::Invalid; // scope operator at the beginning - for (QString::const_iterator iter (input.begin()); iter!=input.end(); ++iter, first = false) - if (!isValid (*iter, first)) - return QValidator::Invalid; + if (scope) + { + scope = false; + prevScope = true; + } + else + { + if (prevScope) + return QValidator::Invalid; // sequence of two scope operators + + scope = true; + } + } + else if (scope) + return QValidator::Invalid; // incomplete scope operator + else + { + prevScope = false; + + if (!isValid (*iter, first)) + return QValidator::Invalid; + } + } + + if (scope) + { + mError = "ID ending with incomplete scope operator"; + return QValidator::Intermediate; + } + + if (prevScope) + { + mError = "ID ending with scope operator"; + return QValidator::Intermediate; + } } return QValidator::Acceptable; +} + +void CSVWorld::IdValidator::setNamespace (const std::string& namespace_) +{ + mNamespace = Misc::StringUtils::lowerCase (namespace_); +} + +std::string CSVWorld::IdValidator::getError() const +{ + return mError; } \ No newline at end of file diff --git a/apps/opencs/view/world/idvalidator.hpp b/apps/opencs/view/world/idvalidator.hpp index 8ca162440..a9df9580a 100644 --- a/apps/opencs/view/world/idvalidator.hpp +++ b/apps/opencs/view/world/idvalidator.hpp @@ -1,6 +1,8 @@ #ifndef CSV_WORLD_IDVALIDATOR_H #define CSV_WORLD_IDVALIDATOR_H +#include + #include namespace CSVWorld @@ -8,6 +10,8 @@ namespace CSVWorld class IdValidator : public QValidator { bool mRelaxed; + std::string mNamespace; + mutable std::string mError; private: @@ -20,6 +24,14 @@ namespace CSVWorld virtual State validate (QString& input, int& pos) const; + void setNamespace (const std::string& namespace_); + + /// Return a description of the error that resulted in the last call of validate + /// returning QValidator::Intermediate. If the last call to validate returned + /// a different value (or if there was no such call yet), an empty string is + /// returned. + std::string getError() const; + }; } diff --git a/apps/opencs/view/world/infocreator.cpp b/apps/opencs/view/world/infocreator.cpp index f09222930..1d914716b 100644 --- a/apps/opencs/view/world/infocreator.cpp +++ b/apps/opencs/view/world/infocreator.cpp @@ -53,6 +53,22 @@ CSVWorld::InfoCreator::InfoCreator (CSMWorld::Data& data, QUndoStack& undoStack, connect (mTopic, SIGNAL (textChanged (const QString&)), this, SLOT (topicChanged())); } +void CSVWorld::InfoCreator::cloneMode (const std::string& originId, + const CSMWorld::UniversalId::Type type) +{ + CSMWorld::IdTable& infoTable = + dynamic_cast (*getData().getTableModel (getCollectionId())); + + int topicColumn = infoTable.findColumnIndex ( + getCollectionId().getType()==CSMWorld::UniversalId::Type_TopicInfos ? + CSMWorld::Columns::ColumnId_Topic : CSMWorld::Columns::ColumnId_Journal); + + mTopic->setText ( + infoTable.data (infoTable.getModelIndex (originId, topicColumn)).toString()); + + GenericCreator::cloneMode (originId, type); +} + void CSVWorld::InfoCreator::reset() { mTopic->setText (""); diff --git a/apps/opencs/view/world/infocreator.hpp b/apps/opencs/view/world/infocreator.hpp index e9cb7e596..2296a8297 100644 --- a/apps/opencs/view/world/infocreator.hpp +++ b/apps/opencs/view/world/infocreator.hpp @@ -27,6 +27,9 @@ namespace CSVWorld InfoCreator (CSMWorld::Data& data, QUndoStack& undoStack, const CSMWorld::UniversalId& id); + virtual void cloneMode (const std::string& originId, + const CSMWorld::UniversalId::Type type); + virtual void reset(); virtual std::string getErrors() const; diff --git a/apps/opencs/view/world/physicsmanager.cpp b/apps/opencs/view/world/physicsmanager.cpp new file mode 100644 index 000000000..fa8db9e1e --- /dev/null +++ b/apps/opencs/view/world/physicsmanager.cpp @@ -0,0 +1,110 @@ +#include "physicsmanager.hpp" + +#include + +#include + +#include "../render/worldspacewidget.hpp" +#include "physicssystem.hpp" + +namespace CSVWorld +{ + PhysicsManager *PhysicsManager::mPhysicsManagerInstance = 0; + + PhysicsManager::PhysicsManager() + { + assert(!mPhysicsManagerInstance); + mPhysicsManagerInstance = this; + } + + PhysicsManager::~PhysicsManager() + { + std::map::iterator iter = mPhysics.begin(); + for(; iter != mPhysics.end(); ++iter) + delete iter->second; // shouldn't be any left but just in case + } + + PhysicsManager *PhysicsManager::instance() + { + assert(mPhysicsManagerInstance); + return mPhysicsManagerInstance; + } + + // create a physics instance per document, called from CSVDoc::View() to get Document* + void PhysicsManager::setupPhysics(CSMDoc::Document *doc) + { + std::map >::iterator iter = mSceneWidgets.find(doc); + if(iter == mSceneWidgets.end()) + { + mSceneWidgets[doc] = std::list (); // zero elements + mPhysics[doc] = new PhysicsSystem(); + } + } + + // destroy physics, called from CSVDoc::ViewManager + void PhysicsManager::removeDocument(CSMDoc::Document *doc) + { + std::map::iterator iter = mPhysics.find(doc); + if(iter != mPhysics.end()) + { + delete iter->second; + mPhysics.erase(iter); + } + + std::map >::iterator it = mSceneWidgets.find(doc); + if(it != mSceneWidgets.end()) + { + mSceneWidgets.erase(it); + } + + // cleanup global resources used by OEngine + if(mPhysics.empty()) + { + delete OEngine::Physic::BulletShapeManager::getSingletonPtr(); + } + } + + // called from CSVRender::WorldspaceWidget() to get widgets' association with Document& + PhysicsSystem *PhysicsManager::addSceneWidget(CSMDoc::Document &doc, CSVRender::WorldspaceWidget *widget) + { + CSVRender::SceneWidget *sceneWidget = static_cast(widget); + + std::map >::iterator iter = mSceneWidgets.begin(); + for(; iter != mSceneWidgets.end(); ++iter) + { + if((*iter).first == &doc) + { + (*iter).second.push_back(sceneWidget); + return mPhysics[(*iter).first]; // TODO: consider using shared_ptr instead + } + } + + throw std::runtime_error("No physics system found for the given document."); + } + + // deprecated by removeDocument() and may be deleted in future code updates + // however there may be some value in removing the deleted scene widgets from the + // list so that the list does not grow forever + void PhysicsManager::removeSceneWidget(CSVRender::WorldspaceWidget *widget) + { + CSVRender::SceneWidget *sceneWidget = static_cast(widget); + + std::map >::iterator iter = mSceneWidgets.begin(); + for(; iter != mSceneWidgets.end(); ++iter) + { + std::list::iterator itWidget = (*iter).second.begin(); + for(; itWidget != (*iter).second.end(); ++itWidget) + { + if((*itWidget) == sceneWidget) + { + (*iter).second.erase(itWidget); + + //if((*iter).second.empty()) // last one for the document + // NOTE: do not delete physics until the document itself is closed + + break; + } + } + } + } +} diff --git a/apps/opencs/view/world/physicsmanager.hpp b/apps/opencs/view/world/physicsmanager.hpp new file mode 100644 index 000000000..e17c9ac84 --- /dev/null +++ b/apps/opencs/view/world/physicsmanager.hpp @@ -0,0 +1,54 @@ +#ifndef CSV_WORLD_PHYSICSMANAGER_H +#define CSV_WORLD_PHYSICSMANAGER_H + +#include +#include + +namespace Ogre +{ + class SceneManager; +} + +namespace CSMDoc +{ + class Document; +} + +namespace CSVRender +{ + class WorldspaceWidget; + class SceneWidget; +} + +namespace CSVWorld +{ + class PhysicsSystem; +} + +namespace CSVWorld +{ + class PhysicsManager + { + static PhysicsManager *mPhysicsManagerInstance; + + std::map > mSceneWidgets; + std::map mPhysics; + + public: + + PhysicsManager(); + ~PhysicsManager(); + + static PhysicsManager *instance(); + + void setupPhysics(CSMDoc::Document *); + + PhysicsSystem *addSceneWidget(CSMDoc::Document &doc, CSVRender::WorldspaceWidget *widget); + + void removeSceneWidget(CSVRender::WorldspaceWidget *widget); + + void removeDocument(CSMDoc::Document *doc); + }; +} + +#endif // CSV_WORLD_PHYSICSMANAGER_H diff --git a/apps/opencs/view/world/physicssystem.cpp b/apps/opencs/view/world/physicssystem.cpp new file mode 100644 index 000000000..57909f4d3 --- /dev/null +++ b/apps/opencs/view/world/physicssystem.cpp @@ -0,0 +1,324 @@ +#include "physicssystem.hpp" + +#include + +#include +#include +#include + +#include +#include +#include "../../model/settings/usersettings.hpp" +#include "../render/elements.hpp" + +namespace CSVWorld +{ + PhysicsSystem::PhysicsSystem() + { + // Create physics. shapeLoader is deleted by the physic engine + NifBullet::ManualBulletShapeLoader* shapeLoader = new NifBullet::ManualBulletShapeLoader(); + mEngine = new OEngine::Physic::PhysicEngine(shapeLoader); + } + + PhysicsSystem::~PhysicsSystem() + { + delete mEngine; + } + + // looks up the scene manager based on the scene node name (inefficient) + // NOTE: referenceId is assumed to be unique per document + // NOTE: searching is done here rather than after rayTest, hence slower to load but + // faster to find (guessing, not verified w/ perf test) + void PhysicsSystem::addObject(const std::string &mesh, + const std::string &sceneNodeName, const std::string &referenceId, float scale, + const Ogre::Vector3 &position, const Ogre::Quaternion &rotation, bool placeable) + { + Ogre::SceneManager *sceneManager = findSceneManager(sceneNodeName); + if(sceneManager) + { + // update maps (NOTE: sometimes replaced) + mSceneNodeToRefId[sceneNodeName] = referenceId; + mSceneNodeToMesh[sceneNodeName] = mesh; + mRefIdToSceneNode[referenceId][sceneManager] = sceneNodeName; + } + else + { + std::cerr << "Attempt to add an object without a corresponding SceneManager: " + + referenceId + " : " + sceneNodeName << std::endl; + return; + } + + // update physics, only one physics model per referenceId + if(mEngine->getRigidBody(referenceId, true) == NULL) + { + mEngine->createAndAdjustRigidBody(mesh, + referenceId, scale, position, rotation, + 0, // scaledBoxTranslation + 0, // boxRotation + true, // raycasting + placeable); + } + } + + // normal delete (e.g closing a scene subview or ~Object()) + // the scene node is destroyed so the mappings should be removed + // + // TODO: should think about using some kind of reference counting within RigidBody + void PhysicsSystem::removeObject(const std::string &sceneNodeName) + { + std::string referenceId = mSceneNodeToRefId[sceneNodeName]; + + if(referenceId != "") + { + mSceneNodeToRefId.erase(sceneNodeName); + mSceneNodeToMesh.erase(sceneNodeName); + + // find which SceneManager has this object + Ogre::SceneManager *sceneManager = findSceneManager(sceneNodeName); + if(!sceneManager) + { + std::cerr << "Attempt to remove an object without a corresponding SceneManager: " + + sceneNodeName << std::endl; + return; + } + + // illustration: erase the object "K" from the object map + // + // RidigBody SubView Ogre + // --------------- -------------- ------------- + // ReferenceId "A" (SceneManager X SceneNode "J") + // (SceneManager Y SceneNode "K") <--- erase + // (SceneManager Z SceneNode "L") + // + // ReferenceId "B" (SceneManager X SceneNode "M") + // (SceneManager Y SceneNode "N") <--- notice not deleted + // (SceneManager Z SceneNode "O") + std::map >::iterator itRef = + mRefIdToSceneNode.begin(); + for(; itRef != mRefIdToSceneNode.end(); ++itRef) + { + if((*itRef).second.find(sceneManager) != (*itRef).second.end()) + { + (*itRef).second.erase(sceneManager); + break; + } + } + + // check whether the physics model should be deleted + if(mRefIdToSceneNode.find(referenceId) == mRefIdToSceneNode.end()) + { + mEngine->removeRigidBody(referenceId); + mEngine->deleteRigidBody(referenceId); + } + } + } + + // Object::clear() is called when reference data is changed. It clears all + // contents of the SceneNode and removes the physics object + // + // A new physics object will be created and assigned to this sceneNodeName by + // Object::update() + void PhysicsSystem::removePhysicsObject(const std::string &sceneNodeName) + { + std::string referenceId = mSceneNodeToRefId[sceneNodeName]; + + if(referenceId != "") + { + mEngine->removeRigidBody(referenceId); + mEngine->deleteRigidBody(referenceId); + } + } + + void PhysicsSystem::replaceObject(const std::string &sceneNodeName, float scale, + const Ogre::Vector3 &position, const Ogre::Quaternion &rotation, bool placeable) + { + std::string referenceId = mSceneNodeToRefId[sceneNodeName]; + std::string mesh = mSceneNodeToMesh[sceneNodeName]; + + if(referenceId != "") + { + // delete the physics object + mEngine->removeRigidBody(referenceId); + mEngine->deleteRigidBody(referenceId); + + // create a new physics object + mEngine->createAndAdjustRigidBody(mesh, referenceId, scale, position, rotation, + 0, 0, true, placeable); + + // update other scene managers if they have the referenceId + // FIXME: rotation or scale not updated + moveSceneNodeImpl(sceneNodeName, referenceId, position); + } + } + + // FIXME: adjustRigidBody() seems to lose objects, work around by deleting and recreating objects + void PhysicsSystem::moveObject(const std::string &sceneNodeName, + const Ogre::Vector3 &position, const Ogre::Quaternion &rotation) + { + mEngine->adjustRigidBody(mEngine->getRigidBody(sceneNodeName, true /*raycasting*/), + position, rotation); + } + + void PhysicsSystem::moveSceneNodeImpl(const std::string sceneNodeName, + const std::string referenceId, const Ogre::Vector3 &position) + { + std::map::const_iterator iter = mSceneWidgets.begin(); + for(; iter != mSceneWidgets.end(); ++iter) + { + std::string name = refIdToSceneNode(referenceId, (*iter).first); + if(name != sceneNodeName && (*iter).first->hasSceneNode(name)) + { + (*iter).first->getSceneNode(name)->setPosition(position); + } + } + } + + void PhysicsSystem::moveSceneNodes(const std::string sceneNodeName, const Ogre::Vector3 &position) + { + moveSceneNodeImpl(sceneNodeName, sceneNodeToRefId(sceneNodeName), position); + } + + void PhysicsSystem::addHeightField(Ogre::SceneManager *sceneManager, + float* heights, int x, int y, float yoffset, float triSize, float sqrtVerts) + { + std::string name = "HeightField_" + + QString::number(x).toStdString() + "_" + QString::number(y).toStdString(); + + if(mTerrain.find(name) == mTerrain.end()) + mEngine->addHeightField(heights, x, y, yoffset, triSize, sqrtVerts); + + mTerrain.insert(std::pair(name, sceneManager)); + } + + void PhysicsSystem::removeHeightField(Ogre::SceneManager *sceneManager, int x, int y) + { + std::string name = "HeightField_" + + QString::number(x).toStdString() + "_" + QString::number(y).toStdString(); + + if(mTerrain.count(name) == 1) + mEngine->removeHeightField(x, y); + + std::multimap::iterator iter = mTerrain.begin(); + for(; iter != mTerrain.end(); ++iter) + { + if((*iter).second == sceneManager) + { + mTerrain.erase(iter); + break; + } + } + } + + // sceneMgr: to lookup the scene node name from the object's referenceId + // camera: primarily used to get the visibility mask for the viewport + // + // returns the found object's scene node name and its position in the world space + // + // WARNING: far clip distance is a global setting, if it changes in future + // this method will need to be updated + std::pair PhysicsSystem::castRay(float mouseX, + float mouseY, Ogre::SceneManager *sceneMgr, Ogre::Camera *camera) + { + // NOTE: there could be more than one camera for the scene manager + // TODO: check whether camera belongs to sceneMgr + if(!sceneMgr || !camera || !camera->getViewport()) + return std::make_pair("", Ogre::Vector3(0,0,0)); // FIXME: this should be an exception + + // using a really small value seems to mess up with the projections + float nearClipDistance = camera->getNearClipDistance(); // save existing + camera->setNearClipDistance(10.0f); // arbitrary number + Ogre::Ray ray = camera->getCameraToViewportRay(mouseX, mouseY); + camera->setNearClipDistance(nearClipDistance); // restore + + Ogre::Vector3 from = ray.getOrigin(); + CSMSettings::UserSettings &userSettings = CSMSettings::UserSettings::instance(); + float farClipDist = userSettings.setting("Scene/far clip distance", QString("300000")).toFloat(); + Ogre::Vector3 to = ray.getPoint(farClipDist); + + btVector3 _from, _to; + _from = btVector3(from.x, from.y, from.z); + _to = btVector3(to.x, to.y, to.z); + + uint32_t visibilityMask = camera->getViewport()->getVisibilityMask(); + bool ignoreHeightMap = !(visibilityMask & (uint32_t)CSVRender::Element_Terrain); + bool ignoreObjects = !(visibilityMask & (uint32_t)CSVRender::Element_Reference); + + Ogre::Vector3 norm; // not used + std::pair result = + mEngine->rayTest(_from, _to, !ignoreObjects, ignoreHeightMap, &norm); + + // result.first is the object's referenceId + if(result.first == "") + return std::make_pair("", Ogre::Vector3(0,0,0)); + else + { + std::string name = refIdToSceneNode(result.first, sceneMgr); + if(name == "") + name = result.first; + else + name = refIdToSceneNode(result.first, sceneMgr); + + return std::make_pair(name, ray.getPoint(farClipDist*result.second)); + } + } + + std::string PhysicsSystem::refIdToSceneNode(std::string referenceId, Ogre::SceneManager *sceneMgr) + { + return mRefIdToSceneNode[referenceId][sceneMgr]; + } + + std::string PhysicsSystem::sceneNodeToRefId(std::string sceneNodeName) + { + return mSceneNodeToRefId[sceneNodeName]; + } + + void PhysicsSystem::addSceneManager(Ogre::SceneManager *sceneMgr, CSVRender::SceneWidget *sceneWidget) + { + mSceneWidgets[sceneMgr] = sceneWidget; + + mEngine->createDebugDraw(sceneMgr); + } + + std::map PhysicsSystem::sceneWidgets() + { + return mSceneWidgets; + } + + void PhysicsSystem::removeSceneManager(Ogre::SceneManager *sceneMgr) + { + mEngine->removeDebugDraw(sceneMgr); + + mSceneWidgets.erase(sceneMgr); + } + + Ogre::SceneManager *PhysicsSystem::findSceneManager(std::string sceneNodeName) + { + std::map::const_iterator iter = mSceneWidgets.begin(); + for(; iter != mSceneWidgets.end(); ++iter) + { + if((*iter).first->hasSceneNode(sceneNodeName)) + { + return (*iter).first; + } + } + + return NULL; + } + + void PhysicsSystem::toggleDebugRendering(Ogre::SceneManager *sceneMgr) + { + // FIXME: should check if sceneMgr is in the list + if(!sceneMgr) + return; + + CSMSettings::UserSettings &userSettings = CSMSettings::UserSettings::instance(); + if(!(userSettings.setting("debug/mouse-picking", QString("false")) == "true" ? true : false)) + { + std::cerr << "Turn on mouse-picking debug option to see collision shapes." << std::endl; + return; + } + + mEngine->toggleDebugRendering(sceneMgr); + mEngine->stepDebug(sceneMgr); + } +} diff --git a/apps/opencs/view/world/physicssystem.hpp b/apps/opencs/view/world/physicssystem.hpp new file mode 100644 index 000000000..0036bf769 --- /dev/null +++ b/apps/opencs/view/world/physicssystem.hpp @@ -0,0 +1,94 @@ +#ifndef CSV_WORLD_PHYSICSSYSTEM_H +#define CSV_WORLD_PHYSICSSYSTEM_H + +#include +#include + +namespace Ogre +{ + class Vector3; + class Quaternion; + class SceneManager; + class Camera; +} + +namespace OEngine +{ + namespace Physic + { + class PhysicEngine; + } +} + +namespace CSVRender +{ + class SceneWidget; +} + +namespace CSVWorld +{ + class PhysicsSystem + { + std::map mSceneNodeToRefId; + std::map > mRefIdToSceneNode; + std::map mSceneNodeToMesh; + std::map mSceneWidgets; + OEngine::Physic::PhysicEngine* mEngine; + std::multimap mTerrain; + + public: + + PhysicsSystem(); + ~PhysicsSystem(); + + void addSceneManager(Ogre::SceneManager *sceneMgr, CSVRender::SceneWidget * scene); + + void removeSceneManager(Ogre::SceneManager *sceneMgr); + + void addObject(const std::string &mesh, + const std::string &sceneNodeName, const std::string &referenceId, float scale, + const Ogre::Vector3 &position, const Ogre::Quaternion &rotation, + bool placeable=false); + + void removeObject(const std::string &sceneNodeName); + void removePhysicsObject(const std::string &sceneNodeName); + + void replaceObject(const std::string &sceneNodeName, + float scale, const Ogre::Vector3 &position, + const Ogre::Quaternion &rotation, bool placeable=false); + + void moveObject(const std::string &sceneNodeName, + const Ogre::Vector3 &position, const Ogre::Quaternion &rotation); + + void moveSceneNodes(const std::string sceneNodeName, const Ogre::Vector3 &position); + + void addHeightField(Ogre::SceneManager *sceneManager, + float* heights, int x, int y, float yoffset, float triSize, float sqrtVerts); + + void removeHeightField(Ogre::SceneManager *sceneManager, int x, int y); + + void toggleDebugRendering(Ogre::SceneManager *sceneMgr); + + // return the object's SceneNode name and position for the given SceneManager + std::pair castRay(float mouseX, + float mouseY, Ogre::SceneManager *sceneMgr, Ogre::Camera *camera); + + std::string sceneNodeToRefId(std::string sceneNodeName); + + // for multi-scene manager per physics engine + std::map sceneWidgets(); + + private: + + void moveSceneNodeImpl(const std::string sceneNodeName, + const std::string referenceId, const Ogre::Vector3 &position); + + void updateSelectionHighlight(std::string sceneNode, const Ogre::Vector3 &position); + + std::string refIdToSceneNode(std::string referenceId, Ogre::SceneManager *sceneMgr); + + Ogre::SceneManager *findSceneManager(std::string sceneNodeName); + }; +} + +#endif // CSV_WORLD_PHYSICSSYSTEM_H diff --git a/apps/opencs/view/world/previewsubview.cpp b/apps/opencs/view/world/previewsubview.cpp index e9a30e65d..1ae466f42 100644 --- a/apps/opencs/view/world/previewsubview.cpp +++ b/apps/opencs/view/world/previewsubview.cpp @@ -5,11 +5,11 @@ #include "../render/previewwidget.hpp" -#include "scenetoolbar.hpp" -#include "scenetoolmode.hpp" +#include "../widget/scenetoolbar.hpp" +#include "../widget/scenetoolmode.hpp" CSVWorld::PreviewSubView::PreviewSubView (const CSMWorld::UniversalId& id, CSMDoc::Document& document) -: SubView (id) +: SubView (id), mTitle (id.toString().c_str()) { QHBoxLayout *layout = new QHBoxLayout; @@ -23,14 +23,14 @@ CSVWorld::PreviewSubView::PreviewSubView (const CSMWorld::UniversalId& id, CSMDo referenceableIdChanged (referenceableId); mScene = - new CSVRender::PreviewWidget (document.getData(), referenceableId, id.getId(), this); + new CSVRender::PreviewWidget (document.getData(), id.getId(), false, this); } else - mScene = new CSVRender::PreviewWidget (document.getData(), id.getId(), this); + mScene = new CSVRender::PreviewWidget (document.getData(), id.getId(), true, this); - SceneToolbar *toolbar = new SceneToolbar (48+6, this); + CSVWidget::SceneToolbar *toolbar = new CSVWidget::SceneToolbar (48+6, this); - SceneToolMode *lightingTool = mScene->makeLightingSelector (toolbar); + CSVWidget::SceneToolMode *lightingTool = mScene->makeLightingSelector (toolbar); toolbar->addTool (lightingTool); layout->addWidget (toolbar, 0); @@ -46,19 +46,25 @@ CSVWorld::PreviewSubView::PreviewSubView (const CSMWorld::UniversalId& id, CSMDo connect (mScene, SIGNAL (closeRequest()), this, SLOT (closeRequest())); connect (mScene, SIGNAL (referenceableIdChanged (const std::string&)), this, SLOT (referenceableIdChanged (const std::string&))); + connect (mScene, SIGNAL (focusToolbarRequest()), toolbar, SLOT (setFocus())); + connect (toolbar, SIGNAL (focusSceneRequest()), mScene, SLOT (setFocus())); } void CSVWorld::PreviewSubView::setEditLock (bool locked) {} -void CSVWorld::PreviewSubView::closeRequest() +std::string CSVWorld::PreviewSubView::getTitle() const { - deleteLater(); + return mTitle; } void CSVWorld::PreviewSubView::referenceableIdChanged (const std::string& id) { if (id.empty()) - setWindowTitle ("Preview: Reference to "); + mTitle = "Preview: Reference to "; else - setWindowTitle (("Preview: Reference to " + id).c_str()); + mTitle = "Preview: Reference to " + id; + + setWindowTitle (QString::fromUtf8 (mTitle.c_str())); + + emit updateTitle(); } \ No newline at end of file diff --git a/apps/opencs/view/world/previewsubview.hpp b/apps/opencs/view/world/previewsubview.hpp index 4ca25c3cb..a28be5c36 100644 --- a/apps/opencs/view/world/previewsubview.hpp +++ b/apps/opencs/view/world/previewsubview.hpp @@ -20,6 +20,7 @@ namespace CSVWorld Q_OBJECT CSVRender::PreviewWidget *mScene; + std::string mTitle; public: @@ -27,9 +28,9 @@ namespace CSVWorld virtual void setEditLock (bool locked); - private slots: + virtual std::string getTitle() const; - void closeRequest(); + private slots: void referenceableIdChanged (const std::string& id); }; diff --git a/apps/opencs/view/world/recordstatusdelegate.cpp b/apps/opencs/view/world/recordstatusdelegate.cpp index 4fe7031ce..708a78015 100644 --- a/apps/opencs/view/world/recordstatusdelegate.cpp +++ b/apps/opencs/view/world/recordstatusdelegate.cpp @@ -9,16 +9,16 @@ CSVWorld::RecordStatusDelegate::RecordStatusDelegate(const ValueList& values, const IconList & icons, - QUndoStack &undoStack, QObject *parent) - : DataDisplayDelegate (values, icons, undoStack, - "Display Format", "Record Status Display", + CSMDoc::Document& document, QObject *parent) + : DataDisplayDelegate (values, icons, document, + "records", "status-format", parent) {} -CSVWorld::CommandDelegate *CSVWorld::RecordStatusDelegateFactory::makeDelegate (QUndoStack& undoStack, - QObject *parent) const +CSVWorld::CommandDelegate *CSVWorld::RecordStatusDelegateFactory::makeDelegate ( + CSMDoc::Document& document, QObject *parent) const { - return new RecordStatusDelegate (mValues, mIcons, undoStack, parent); + return new RecordStatusDelegate (mValues, mIcons, document, parent); } CSVWorld::RecordStatusDelegateFactory::RecordStatusDelegateFactory() diff --git a/apps/opencs/view/world/recordstatusdelegate.hpp b/apps/opencs/view/world/recordstatusdelegate.hpp index 1b42223af..fbdaed538 100644 --- a/apps/opencs/view/world/recordstatusdelegate.hpp +++ b/apps/opencs/view/world/recordstatusdelegate.hpp @@ -19,7 +19,7 @@ namespace CSVWorld explicit RecordStatusDelegate(const ValueList& values, const IconList& icons, - QUndoStack& undoStack, QObject *parent = 0); + CSMDoc::Document& document, QObject *parent = 0); }; class RecordStatusDelegateFactory : public DataDisplayDelegateFactory @@ -28,7 +28,7 @@ namespace CSVWorld RecordStatusDelegateFactory(); - virtual CommandDelegate *makeDelegate (QUndoStack& undoStack, QObject *parent) const; + virtual CommandDelegate *makeDelegate (CSMDoc::Document& document, QObject *parent) const; ///< The ownership of the returned CommandDelegate is transferred to the caller. }; diff --git a/apps/opencs/view/world/referenceablecreator.cpp b/apps/opencs/view/world/referenceablecreator.cpp index 7a5fca853..e8055ed31 100644 --- a/apps/opencs/view/world/referenceablecreator.cpp +++ b/apps/opencs/view/world/referenceablecreator.cpp @@ -42,6 +42,13 @@ void CSVWorld::ReferenceableCreator::reset() GenericCreator::reset(); } +void CSVWorld::ReferenceableCreator::cloneMode (const std::string& originId, + const CSMWorld::UniversalId::Type type) +{ + GenericCreator::cloneMode (originId, type); + mType->setCurrentIndex (mType->findData (static_cast (type))); +} + void CSVWorld::ReferenceableCreator::toggleWidgets(bool active) { CSVWorld::GenericCreator::toggleWidgets(active); diff --git a/apps/opencs/view/world/referenceablecreator.hpp b/apps/opencs/view/world/referenceablecreator.hpp index 88545575e..14ad24b29 100644 --- a/apps/opencs/view/world/referenceablecreator.hpp +++ b/apps/opencs/view/world/referenceablecreator.hpp @@ -23,6 +23,10 @@ namespace CSVWorld const CSMWorld::UniversalId& id); virtual void reset(); + + virtual void cloneMode (const std::string& originId, + const CSMWorld::UniversalId::Type type); + virtual void toggleWidgets(bool active = true); }; diff --git a/apps/opencs/view/world/referencecreator.cpp b/apps/opencs/view/world/referencecreator.cpp index 6b8e02da0..1e3cc00d7 100644 --- a/apps/opencs/view/world/referencecreator.cpp +++ b/apps/opencs/view/world/referencecreator.cpp @@ -16,11 +16,58 @@ std::string CSVWorld::ReferenceCreator::getId() const void CSVWorld::ReferenceCreator::configureCreateCommand (CSMWorld::CreateCommand& command) const { - int index = + // Set cellID + int cellIdColumn = dynamic_cast (*getData().getTableModel (getCollectionId())). findColumnIndex (CSMWorld::Columns::ColumnId_Cell); - command.addValue (index, mCell->text()); + command.addValue (cellIdColumn, mCell->text()); + + // Set RefNum + int refNumColumn = dynamic_cast ( + *getData().getTableModel (CSMWorld::UniversalId::Type_References)). + findColumnIndex (CSMWorld::Columns::ColumnId_RefNum); + + command.addValue (refNumColumn, getRefNumCount()); +} + +void CSVWorld::ReferenceCreator::pushCommand (std::auto_ptr command, + const std::string& id) +{ + // get the old count + std::string cellId = mCell->text().toUtf8().constData(); + + CSMWorld::IdTable& cellTable = dynamic_cast ( + *getData().getTableModel (CSMWorld::UniversalId::Type_Cells)); + + int countColumn = cellTable.findColumnIndex (CSMWorld::Columns::ColumnId_RefNumCounter); + + QModelIndex countIndex = cellTable.getModelIndex (cellId, countColumn); + + int count = cellTable.data (countIndex).toInt(); + + // command for incrementing counter + std::auto_ptr increment (new CSMWorld::ModifyCommand + (cellTable, countIndex, count+1)); + + getUndoStack().beginMacro (command->text()); + GenericCreator::pushCommand (command, id); + getUndoStack().push (increment.release()); + getUndoStack().endMacro(); +} + +int CSVWorld::ReferenceCreator::getRefNumCount() const +{ + std::string cellId = mCell->text().toUtf8().constData(); + + CSMWorld::IdTable& cellTable = dynamic_cast ( + *getData().getTableModel (CSMWorld::UniversalId::Type_Cells)); + + int countColumn = cellTable.findColumnIndex (CSMWorld::Columns::ColumnId_RefNumCounter); + + QModelIndex countIndex = cellTable.getModelIndex (cellId, countColumn); + + return cellTable.data (countIndex).toInt(); } CSVWorld::ReferenceCreator::ReferenceCreator (CSMWorld::Data& data, QUndoStack& undoStack, @@ -47,12 +94,9 @@ void CSVWorld::ReferenceCreator::reset() std::string CSVWorld::ReferenceCreator::getErrors() const { - std::string errors = GenericCreator::getErrors(); - - if (mCloneMode) - { - return errors; - } + // We are ignoring errors coming from GenericCreator here, because the ID of the new + // record is internal and requires neither user input nor verification. + std::string errors; std::string cell = mCell->text().toUtf8().constData(); @@ -79,15 +123,17 @@ void CSVWorld::ReferenceCreator::cellChanged() update(); } -void CSVWorld::ReferenceCreator::toggleWidgets(bool active) -{ - CSVWorld::GenericCreator::toggleWidgets(active); - mCell->setEnabled(active); -} - void CSVWorld::ReferenceCreator::cloneMode(const std::string& originId, const CSMWorld::UniversalId::Type type) { + CSMWorld::IdTable& referenceTable = dynamic_cast ( + *getData().getTableModel (CSMWorld::UniversalId::Type_References)); + + int cellIdColumn = referenceTable.findColumnIndex (CSMWorld::Columns::ColumnId_Cell); + + mCell->setText ( + referenceTable.data (referenceTable.getModelIndex (originId, cellIdColumn)).toString()); + CSVWorld::GenericCreator::cloneMode(originId, type); cellChanged(); //otherwise ok button will remain disabled } diff --git a/apps/opencs/view/world/referencecreator.hpp b/apps/opencs/view/world/referencecreator.hpp index 12fb12dd9..002a62d87 100644 --- a/apps/opencs/view/world/referencecreator.hpp +++ b/apps/opencs/view/world/referencecreator.hpp @@ -20,16 +20,20 @@ namespace CSVWorld virtual void configureCreateCommand (CSMWorld::CreateCommand& command) const; + virtual void pushCommand (std::auto_ptr command, + const std::string& id); + + int getRefNumCount() const; + public: ReferenceCreator (CSMWorld::Data& data, QUndoStack& undoStack, const CSMWorld::UniversalId& id); - virtual void cloneMode(const std::string& originId, + virtual void cloneMode(const std::string& originId, const CSMWorld::UniversalId::Type type); virtual void reset(); - virtual void toggleWidgets(bool active = true); virtual std::string getErrors() const; ///< Return formatted error descriptions for the current state of the creator. if an empty diff --git a/apps/opencs/view/world/regionmap.cpp b/apps/opencs/view/world/regionmap.cpp index 849a1988a..9497e4054 100644 --- a/apps/opencs/view/world/regionmap.cpp +++ b/apps/opencs/view/world/regionmap.cpp @@ -382,6 +382,9 @@ void CSVWorld::RegionMap::dropEvent (QDropEvent* event) } const CSMWorld::TableMimeData* mime = dynamic_cast (event->mimeData()); + if (!mime) // May happen when non-records (e.g. plain text) are dragged and dropped + return; + if (mime->fromDocument(mDocument) && mime->holdsType(CSMWorld::UniversalId::Type_Region)) { CSMWorld::UniversalId record (mime->returnMatching (CSMWorld::UniversalId::Type_Region)); @@ -402,4 +405,4 @@ void CSVWorld::RegionMap::dropEvent (QDropEvent* event) mRegionId = record.getId(); } -} \ No newline at end of file +} diff --git a/apps/opencs/view/world/scenesubview.cpp b/apps/opencs/view/world/scenesubview.cpp index 36cce9ecd..5ddefcc6b 100644 --- a/apps/opencs/view/world/scenesubview.cpp +++ b/apps/opencs/view/world/scenesubview.cpp @@ -17,9 +17,13 @@ #include "../render/pagedworldspacewidget.hpp" #include "../render/unpagedworldspacewidget.hpp" +#include "../widget/scenetoolbar.hpp" +#include "../widget/scenetoolmode.hpp" +#include "../widget/scenetooltoggle.hpp" +#include "../widget/scenetoolrun.hpp" + #include "tablebottombox.hpp" #include "creator.hpp" -#include "scenetoolmode.hpp" CSVWorld::SceneSubView::SceneSubView (const CSMWorld::UniversalId& id, CSMDoc::Document& document) : SubView (id), mLayout(new QHBoxLayout), mDocument(document), mScene(NULL), mToolbar(NULL) @@ -34,7 +38,7 @@ CSVWorld::SceneSubView::SceneSubView (const CSMWorld::UniversalId& id, CSMDoc::D mLayout->setContentsMargins (QMargins (0, 0, 0, 0)); - CSVRender::WorldspaceWidget* wordspaceWidget = NULL; + CSVRender::WorldspaceWidget* worldspaceWidget = NULL; widgetType whatWidget; if (id.getId()=="sys::default") @@ -43,7 +47,7 @@ CSVWorld::SceneSubView::SceneSubView (const CSMWorld::UniversalId& id, CSMDoc::D CSVRender::PagedWorldspaceWidget *newWidget = new CSVRender::PagedWorldspaceWidget (this, document); - wordspaceWidget = newWidget; + worldspaceWidget = newWidget; makeConnections(newWidget); } @@ -53,12 +57,12 @@ CSVWorld::SceneSubView::SceneSubView (const CSMWorld::UniversalId& id, CSMDoc::D CSVRender::UnpagedWorldspaceWidget *newWidget = new CSVRender::UnpagedWorldspaceWidget (id.getId(), document, this); - wordspaceWidget = newWidget; + worldspaceWidget = newWidget; makeConnections(newWidget); } - replaceToolbarAndWorldspace(wordspaceWidget, makeToolbar(wordspaceWidget, whatWidget)); + replaceToolbarAndWorldspace(worldspaceWidget, makeToolbar(worldspaceWidget, whatWidget)); layout->insertLayout (0, mLayout, 1); @@ -95,41 +99,46 @@ void CSVWorld::SceneSubView::makeConnections (CSVRender::PagedWorldspaceWidget* this, SLOT (cellSelectionChanged (const CSMWorld::CellSelection&))); } -CSVWorld::SceneToolbar* CSVWorld::SceneSubView::makeToolbar (CSVRender::WorldspaceWidget* widget, widgetType type) +CSVWidget::SceneToolbar* CSVWorld::SceneSubView::makeToolbar (CSVRender::WorldspaceWidget* widget, widgetType type) { - CSVWorld::SceneToolbar* toolbar = new SceneToolbar (48+6, this); + CSVWidget::SceneToolbar* toolbar = new CSVWidget::SceneToolbar (48+6, this); - SceneToolMode *navigationTool = widget->makeNavigationSelector (toolbar); + CSVWidget::SceneToolMode *navigationTool = widget->makeNavigationSelector (toolbar); toolbar->addTool (navigationTool); - SceneToolMode *lightingTool = widget->makeLightingSelector (toolbar); + CSVWidget::SceneToolMode *lightingTool = widget->makeLightingSelector (toolbar); toolbar->addTool (lightingTool); -/* Add buttons specific to the type. For now no need for it. - * - switch (type) - { - case widget_Paged: - break; + CSVWidget::SceneToolToggle *sceneVisibilityTool = + widget->makeSceneVisibilitySelector (toolbar); + toolbar->addTool (sceneVisibilityTool); - case widget_Unpaged: - break; + if (type==widget_Paged) + { + CSVWidget::SceneToolToggle *controlVisibilityTool = + dynamic_cast (*widget). + makeControlVisibilitySelector (toolbar); + toolbar->addTool (controlVisibilityTool); } -*/ + + CSVWidget::SceneToolRun *runTool = widget->makeRunTool (toolbar); + toolbar->addTool (runTool); + + CSVWidget::SceneToolMode *editModeTool = widget->makeEditModeSelector (toolbar); + toolbar->addTool (editModeTool); + return toolbar; } void CSVWorld::SceneSubView::setEditLock (bool locked) { - } void CSVWorld::SceneSubView::updateEditorSetting(const QString &settingName, const QString &settingValue) { - } void CSVWorld::SceneSubView::setStatusBar (bool show) @@ -142,9 +151,9 @@ void CSVWorld::SceneSubView::useHint (const std::string& hint) mScene->useViewHint (hint); } -void CSVWorld::SceneSubView::closeRequest() +std::string CSVWorld::SceneSubView::getTitle() const { - deleteLater(); + return mTitle; } void CSVWorld::SceneSubView::cellSelectionChanged (const CSMWorld::UniversalId& id) @@ -153,10 +162,11 @@ void CSVWorld::SceneSubView::cellSelectionChanged (const CSMWorld::UniversalId& std::ostringstream stream; stream << "Scene: " << getUniversalId().getId(); - setWindowTitle (QString::fromUtf8 (stream.str().c_str())); + mTitle = stream.str(); + setWindowTitle (QString::fromUtf8 (mTitle.c_str())); + emit updateTitle(); } - void CSVWorld::SceneSubView::cellSelectionChanged (const CSMWorld::CellSelection& selection) { setUniversalId(CSMWorld::UniversalId(CSMWorld::UniversalId::Type_Scene, "sys::default")); @@ -181,19 +191,23 @@ void CSVWorld::SceneSubView::cellSelectionChanged (const CSMWorld::CellSelection stream << "cell around it)"; } - setWindowTitle (QString::fromUtf8 (stream.str().c_str())); + mTitle = stream.str(); + setWindowTitle (QString::fromUtf8 (mTitle.c_str())); + emit updateTitle(); } void CSVWorld::SceneSubView::handleDrop (const std::vector< CSMWorld::UniversalId >& data) { CSVRender::PagedWorldspaceWidget* pagedNewWidget = NULL; CSVRender::UnpagedWorldspaceWidget* unPagedNewWidget = NULL; - SceneToolbar* toolbar = NULL; + CSVWidget::SceneToolbar* toolbar = NULL; - switch (mScene->getDropRequirements(CSVRender::WorldspaceWidget::getDropType(data))) + CSVRender::WorldspaceWidget::DropType type = CSVRender::WorldspaceWidget::getDropType (data); + + switch (mScene->getDropRequirements (type)) { case CSVRender::WorldspaceWidget::canHandle: - mScene->handleDrop(data); + mScene->handleDrop (data, type); break; case CSVRender::WorldspaceWidget::needPaged: @@ -201,7 +215,7 @@ void CSVWorld::SceneSubView::handleDrop (const std::vector< CSMWorld::UniversalI toolbar = makeToolbar(pagedNewWidget, widget_Paged); makeConnections(pagedNewWidget); replaceToolbarAndWorldspace(pagedNewWidget, toolbar); - mScene->handleDrop(data); + mScene->handleDrop (data, type); break; case CSVRender::WorldspaceWidget::needUnpaged: @@ -217,7 +231,7 @@ void CSVWorld::SceneSubView::handleDrop (const std::vector< CSMWorld::UniversalI } } -void CSVWorld::SceneSubView::replaceToolbarAndWorldspace (CSVRender::WorldspaceWidget* widget, CSVWorld::SceneToolbar* toolbar) +void CSVWorld::SceneSubView::replaceToolbarAndWorldspace (CSVRender::WorldspaceWidget* widget, CSVWidget::SceneToolbar* toolbar) { assert(mLayout); @@ -236,8 +250,19 @@ void CSVWorld::SceneSubView::replaceToolbarAndWorldspace (CSVRender::WorldspaceW mScene = widget; mToolbar = toolbar; + connect (mScene, SIGNAL (focusToolbarRequest()), mToolbar, SLOT (setFocus())); + connect (this, SIGNAL (updateSceneUserSetting(const QString &, const QStringList &)), + mScene, SLOT (updateUserSetting(const QString &, const QStringList &))); + connect (mToolbar, SIGNAL (focusSceneRequest()), mScene, SLOT (setFocus())); + mLayout->addWidget (mToolbar, 0); mLayout->addWidget (mScene, 1); mScene->selectDefaultNavigationMode(); -} \ No newline at end of file + setFocusProxy (mScene); +} + +void CSVWorld::SceneSubView::updateUserSetting (const QString &key, const QStringList &list) +{ + emit updateSceneUserSetting(key, list); +} diff --git a/apps/opencs/view/world/scenesubview.hpp b/apps/opencs/view/world/scenesubview.hpp index b9ecbe931..fc45347d0 100644 --- a/apps/opencs/view/world/scenesubview.hpp +++ b/apps/opencs/view/world/scenesubview.hpp @@ -4,7 +4,6 @@ #include #include "../doc/subview.hpp" -#include "scenetoolbar.hpp" class QModelIndex; @@ -25,6 +24,11 @@ namespace CSVRender class UnpagedWorldspaceWidget; } +namespace CSVWidget +{ + class SceneToolbar; +} + namespace CSVWorld { class Table; @@ -39,7 +43,8 @@ namespace CSVWorld CSVRender::WorldspaceWidget *mScene; QHBoxLayout* mLayout; CSMDoc::Document& mDocument; - SceneToolbar* mToolbar; + CSVWidget::SceneToolbar* mToolbar; + std::string mTitle; public: @@ -53,30 +58,39 @@ namespace CSVWorld virtual void useHint (const std::string& hint); + virtual std::string getTitle() const; + private: void makeConnections(CSVRender::PagedWorldspaceWidget* widget); void makeConnections(CSVRender::UnpagedWorldspaceWidget* widget); - void replaceToolbarAndWorldspace(CSVRender::WorldspaceWidget* widget, SceneToolbar* toolbar); + void replaceToolbarAndWorldspace(CSVRender::WorldspaceWidget* widget, CSVWidget::SceneToolbar* toolbar); enum widgetType { widget_Paged, widget_Unpaged }; - SceneToolbar* makeToolbar(CSVRender::WorldspaceWidget* widget, widgetType type); - private slots: + CSVWidget::SceneToolbar* makeToolbar(CSVRender::WorldspaceWidget* widget, widgetType type); - void closeRequest(); + private slots: void cellSelectionChanged (const CSMWorld::CellSelection& selection); void cellSelectionChanged (const CSMWorld::UniversalId& id); void handleDrop(const std::vector& data); + + public slots: + + void updateUserSetting (const QString &, const QStringList &); + + signals: + + void updateSceneUserSetting (const QString &, const QStringList &); }; } diff --git a/apps/opencs/view/world/scenetool.cpp b/apps/opencs/view/world/scenetool.cpp deleted file mode 100644 index 612b4c6d3..000000000 --- a/apps/opencs/view/world/scenetool.cpp +++ /dev/null @@ -1,18 +0,0 @@ - -#include "scenetool.hpp" - -#include "scenetoolbar.hpp" - -CSVWorld::SceneTool::SceneTool (SceneToolbar *parent) : QPushButton (parent) -{ - setSizePolicy (QSizePolicy (QSizePolicy::Fixed, QSizePolicy::Fixed)); - setIconSize (QSize (parent->getIconSize(), parent->getIconSize())); - setFixedSize (parent->getButtonSize(), parent->getButtonSize()); - - connect (this, SIGNAL (clicked()), this, SLOT (openRequest())); -} - -void CSVWorld::SceneTool::openRequest() -{ - showPanel (parentWidget()->mapToGlobal (pos())); -} diff --git a/apps/opencs/view/world/scenetool.hpp b/apps/opencs/view/world/scenetool.hpp deleted file mode 100644 index 07e8b58d7..000000000 --- a/apps/opencs/view/world/scenetool.hpp +++ /dev/null @@ -1,27 +0,0 @@ -#ifndef CSV_WORLD_SCENETOOL_H -#define CSV_WORLD_SCENETOOL_H - -#include - -namespace CSVWorld -{ - class SceneToolbar; - - ///< \brief Tool base class - class SceneTool : public QPushButton - { - Q_OBJECT - - public: - - SceneTool (SceneToolbar *parent); - - virtual void showPanel (const QPoint& position) = 0; - - private slots: - - void openRequest(); - }; -} - -#endif diff --git a/apps/opencs/view/world/scenetoolbar.cpp b/apps/opencs/view/world/scenetoolbar.cpp deleted file mode 100644 index d60240da7..000000000 --- a/apps/opencs/view/world/scenetoolbar.cpp +++ /dev/null @@ -1,34 +0,0 @@ - -#include "scenetoolbar.hpp" - -#include - -#include "scenetool.hpp" - -CSVWorld::SceneToolbar::SceneToolbar (int buttonSize, QWidget *parent) -: QWidget (parent), mButtonSize (buttonSize), mIconSize (buttonSize-6) -{ - setFixedWidth (mButtonSize); - - mLayout = new QVBoxLayout (this); - mLayout->setAlignment (Qt::AlignTop); - - mLayout->setContentsMargins (QMargins (0, 0, 0, 0)); - - setLayout (mLayout); -} - -void CSVWorld::SceneToolbar::addTool (SceneTool *tool) -{ - mLayout->addWidget (tool, 0, Qt::AlignTop); -} - -int CSVWorld::SceneToolbar::getButtonSize() const -{ - return mButtonSize; -} - -int CSVWorld::SceneToolbar::getIconSize() const -{ - return mIconSize; -} \ No newline at end of file diff --git a/apps/opencs/view/world/scenetoolbar.hpp b/apps/opencs/view/world/scenetoolbar.hpp deleted file mode 100644 index 731806cc5..000000000 --- a/apps/opencs/view/world/scenetoolbar.hpp +++ /dev/null @@ -1,32 +0,0 @@ -#ifndef CSV_WORLD_SCENETOOLBAR_H -#define CSV_WORLD_SCENETOOLBAR_H - -#include - -class QVBoxLayout; - -namespace CSVWorld -{ - class SceneTool; - - class SceneToolbar : public QWidget - { - Q_OBJECT - - QVBoxLayout *mLayout; - int mButtonSize; - int mIconSize; - - public: - - SceneToolbar (int buttonSize, QWidget *parent = 0); - - void addTool (SceneTool *tool); - - int getButtonSize() const; - - int getIconSize() const; - }; -} - -#endif diff --git a/apps/opencs/view/world/scenetoolmode.cpp b/apps/opencs/view/world/scenetoolmode.cpp deleted file mode 100644 index 73b01ae3a..000000000 --- a/apps/opencs/view/world/scenetoolmode.cpp +++ /dev/null @@ -1,57 +0,0 @@ - -#include "scenetoolmode.hpp" - -#include -#include -#include - -#include "scenetoolbar.hpp" - -CSVWorld::SceneToolMode::SceneToolMode (SceneToolbar *parent) -: SceneTool (parent), mButtonSize (parent->getButtonSize()), mIconSize (parent->getIconSize()) -{ - mPanel = new QFrame (this, Qt::Popup); - - mLayout = new QHBoxLayout (mPanel); - - mLayout->setContentsMargins (QMargins (0, 0, 0, 0)); - - mPanel->setLayout (mLayout); -} - -void CSVWorld::SceneToolMode::showPanel (const QPoint& position) -{ - mPanel->move (position); - mPanel->show(); -} - -void CSVWorld::SceneToolMode::addButton (const std::string& icon, const std::string& id) -{ - QPushButton *button = new QPushButton (QIcon (QPixmap (icon.c_str())), "", mPanel); - button->setSizePolicy (QSizePolicy (QSizePolicy::Fixed, QSizePolicy::Fixed)); - button->setIconSize (QSize (mIconSize, mIconSize)); - button->setFixedSize (mButtonSize, mButtonSize); - - mLayout->addWidget (button); - - mButtons.insert (std::make_pair (button, id)); - - connect (button, SIGNAL (clicked()), this, SLOT (selected())); - - if (mButtons.size()==1) - setIcon (button->icon()); -} - -void CSVWorld::SceneToolMode::selected() -{ - std::map::const_iterator iter = - mButtons.find (dynamic_cast (sender())); - - if (iter!=mButtons.end()) - { - mPanel->hide(); - - setIcon (iter->first->icon()); - emit modeChanged (iter->second); - } -} \ No newline at end of file diff --git a/apps/opencs/view/world/scenetoolmode.hpp b/apps/opencs/view/world/scenetoolmode.hpp deleted file mode 100644 index a156c0c95..000000000 --- a/apps/opencs/view/world/scenetoolmode.hpp +++ /dev/null @@ -1,43 +0,0 @@ -#ifndef CSV_WORLD_SCENETOOL_MODE_H -#define CSV_WORLD_SCENETOOL_MODE_H - -#include "scenetool.hpp" - -#include - -class QHBoxLayout; - -namespace CSVWorld -{ - class SceneToolbar; - - ///< \brief Mode selector tool - class SceneToolMode : public SceneTool - { - Q_OBJECT - - QWidget *mPanel; - QHBoxLayout *mLayout; - std::map mButtons; // widget, id - int mButtonSize; - int mIconSize; - - public: - - SceneToolMode (SceneToolbar *parent); - - virtual void showPanel (const QPoint& position); - - void addButton (const std::string& icon, const std::string& id); - - signals: - - void modeChanged (const std::string& id); - - private slots: - - void selected(); - }; -} - -#endif diff --git a/apps/opencs/view/world/scriptedit.cpp b/apps/opencs/view/world/scriptedit.cpp index b1528d525..c2d94ab5d 100644 --- a/apps/opencs/view/world/scriptedit.cpp +++ b/apps/opencs/view/world/scriptedit.cpp @@ -6,14 +6,35 @@ #include #include +#include "../../model/doc/document.hpp" + #include "../../model/world/universalid.hpp" #include "../../model/world/tablemimedata.hpp" -CSVWorld::ScriptEdit::ScriptEdit (QWidget* parent, const CSMDoc::Document& document) : - QTextEdit (parent), + +CSVWorld::ScriptEdit::ChangeLock::ChangeLock (ScriptEdit& edit) : mEdit (edit) +{ + ++mEdit.mChangeLocked; +} + +CSVWorld::ScriptEdit::ChangeLock::~ChangeLock() +{ + --mEdit.mChangeLocked; +} + + +CSVWorld::ScriptEdit::ScriptEdit (const CSMDoc::Document& document, ScriptHighlighter::Mode mode, + QWidget* parent) + : QPlainTextEdit (parent), mDocument (document), - mWhiteListQoutes("^[a-z|_]{1}[a-z|0-9|_]{0,}$", Qt::CaseInsensitive) + mWhiteListQoutes("^[a-z|_]{1}[a-z|0-9|_]{0,}$", Qt::CaseInsensitive), + mChangeLocked (0) { +// setAcceptRichText (false); + setLineWrapMode (QPlainTextEdit::NoWrap); + setTabStopWidth (4); + setUndoRedoEnabled (false); // we use OpenCS-wide undo/redo instead + mAllowedTypes <pos())); - event->acceptProposedAction(); + const CSMWorld::TableMimeData* mime = dynamic_cast (event->mimeData()); + if (!mime) + QPlainTextEdit::dragEnterEvent(event); + else + { + setTextCursor (cursorForPosition (event->pos())); + event->acceptProposedAction(); + } } void CSVWorld::ScriptEdit::dragMoveEvent (QDragMoveEvent* event) { - setTextCursor (cursorForPosition (event->pos())); - event->accept(); + const CSMWorld::TableMimeData* mime = dynamic_cast (event->mimeData()); + if (!mime) + QPlainTextEdit::dragMoveEvent(event); + else + { + setTextCursor (cursorForPosition (event->pos())); + event->accept(); + } } void CSVWorld::ScriptEdit::dropEvent (QDropEvent* event) { const CSMWorld::TableMimeData* mime = dynamic_cast (event->mimeData()); + if (!mime) // May happen when non-records (e.g. plain text) are dragged and dropped + { + QPlainTextEdit::dropEvent(event); + return; + } setTextCursor (cursorForPosition (event->pos())); @@ -86,3 +139,21 @@ bool CSVWorld::ScriptEdit::stringNeedsQuote (const std::string& id) const //I'm not quite sure when do we need to put quotes. To be safe we will use quotes for anything other thanā€¦ return !(string.contains(mWhiteListQoutes)); } + +void CSVWorld::ScriptEdit::idListChanged() +{ + mHighlighter->invalidateIds(); + + if (!mUpdateTimer.isActive()) + mUpdateTimer.start (0); +} + +void CSVWorld::ScriptEdit::updateHighlighting() +{ + if (isChangeLocked()) + return; + + ChangeLock lock (*this); + + mHighlighter->rehighlight(); +} \ No newline at end of file diff --git a/apps/opencs/view/world/scriptedit.hpp b/apps/opencs/view/world/scriptedit.hpp index b4627c2fe..c67385816 100644 --- a/apps/opencs/view/world/scriptedit.hpp +++ b/apps/opencs/view/world/scriptedit.hpp @@ -1,11 +1,14 @@ #ifndef SCRIPTEDIT_H #define SCRIPTEDIT_H -#include +#include #include +#include #include "../../model/world/universalid.hpp" +#include "scripthighlighter.hpp" + class QWidget; class QRegExp; @@ -16,11 +19,42 @@ namespace CSMDoc namespace CSVWorld { - class ScriptEdit : public QTextEdit + class ScriptEdit : public QPlainTextEdit { Q_OBJECT + public: - ScriptEdit (QWidget* parent, const CSMDoc::Document& document); + + class ChangeLock + { + ScriptEdit& mEdit; + + ChangeLock (const ChangeLock&); + ChangeLock& operator= (const ChangeLock&); + + public: + + ChangeLock (ScriptEdit& edit); + ~ChangeLock(); + }; + + friend class ChangeLock; + + private: + + int mChangeLocked; + ScriptHighlighter *mHighlighter; + QTimer mUpdateTimer; + + public: + + ScriptEdit (const CSMDoc::Document& document, ScriptHighlighter::Mode mode, + QWidget* parent); + + /// Should changes to the data be ignored (i.e. not cause updated)? + /// + /// \note This mechanism is used to avoid infinite update recursions + bool isChangeLocked() const; private: QVector mAllowedTypes; @@ -34,6 +68,12 @@ namespace CSVWorld void dragMoveEvent (QDragMoveEvent* event); bool stringNeedsQuote(const std::string& id) const; + + private slots: + + void idListChanged(); + + void updateHighlighting(); }; } #endif // SCRIPTEDIT_H \ No newline at end of file diff --git a/apps/opencs/view/world/scripthighlighter.cpp b/apps/opencs/view/world/scripthighlighter.cpp index e06dab372..36cebcb76 100644 --- a/apps/opencs/view/world/scripthighlighter.cpp +++ b/apps/opencs/view/world/scripthighlighter.cpp @@ -30,6 +30,16 @@ bool CSVWorld::ScriptHighlighter::parseName (const std::string& name, const Comp bool CSVWorld::ScriptHighlighter::parseKeyword (int keyword, const Compiler::TokenLoc& loc, Compiler::Scanner& scanner) { + if (((mMode==Mode_Console || mMode==Mode_Dialogue) && + (keyword==Compiler::Scanner::K_begin || keyword==Compiler::Scanner::K_end || + keyword==Compiler::Scanner::K_short || keyword==Compiler::Scanner::K_long || + keyword==Compiler::Scanner::K_float)) + || (mMode==Mode_Console && (keyword==Compiler::Scanner::K_if || + keyword==Compiler::Scanner::K_endif || keyword==Compiler::Scanner::K_else || + keyword==Compiler::Scanner::K_elseif || keyword==Compiler::Scanner::K_while || + keyword==Compiler::Scanner::K_endwhile))) + return parseName (loc.mLiteral, loc, scanner); + highlight (loc, Type_Keyword); return true; } @@ -63,8 +73,10 @@ void CSVWorld::ScriptHighlighter::highlight (const Compiler::TokenLoc& loc, Type setFormat (index, length, mScheme[type]); } -CSVWorld::ScriptHighlighter::ScriptHighlighter (const CSMWorld::Data& data, QTextDocument *parent) -: QSyntaxHighlighter (parent), Compiler::Parser (mErrorHandler, mContext), mContext (data) +CSVWorld::ScriptHighlighter::ScriptHighlighter (const CSMWorld::Data& data, Mode mode, + QTextDocument *parent) +: QSyntaxHighlighter (parent), Compiler::Parser (mErrorHandler, mContext), mContext (data), + mMode (mode) { /// \todo replace this with user settings { diff --git a/apps/opencs/view/world/scripthighlighter.hpp b/apps/opencs/view/world/scripthighlighter.hpp index 495c2e6a3..953f2f953 100644 --- a/apps/opencs/view/world/scripthighlighter.hpp +++ b/apps/opencs/view/world/scripthighlighter.hpp @@ -28,12 +28,20 @@ namespace CSVWorld Type_Id }; + enum Mode + { + Mode_General, + Mode_Console, + Mode_Dialogue + }; + private: Compiler::NullErrorHandler mErrorHandler; Compiler::Extensions mExtensions; CSMWorld::ScriptContext mContext; std::map mScheme; + Mode mMode; private: @@ -74,7 +82,7 @@ namespace CSVWorld public: - ScriptHighlighter (const CSMWorld::Data& data, QTextDocument *parent); + ScriptHighlighter (const CSMWorld::Data& data, Mode mode, QTextDocument *parent); virtual void highlightBlock (const QString& text); diff --git a/apps/opencs/view/world/scriptsubview.cpp b/apps/opencs/view/world/scriptsubview.cpp index fa41151ca..22d8e7e51 100644 --- a/apps/opencs/view/world/scriptsubview.cpp +++ b/apps/opencs/view/world/scriptsubview.cpp @@ -3,8 +3,6 @@ #include -#include - #include "../../model/doc/document.hpp" #include "../../model/world/universalid.hpp" #include "../../model/world/data.hpp" @@ -12,28 +10,12 @@ #include "../../model/world/commands.hpp" #include "../../model/world/idtable.hpp" -#include "scripthighlighter.hpp" #include "scriptedit.hpp" -CSVWorld::ScriptSubView::ChangeLock::ChangeLock (ScriptSubView& view) : mView (view) -{ - ++mView.mChangeLocked; -} - -CSVWorld::ScriptSubView::ChangeLock::~ChangeLock() -{ - --mView.mChangeLocked; -} - CSVWorld::ScriptSubView::ScriptSubView (const CSMWorld::UniversalId& id, CSMDoc::Document& document) -: SubView (id), mDocument (document), mColumn (-1), mChangeLocked (0) +: SubView (id), mDocument (document), mColumn (-1) { - setWidget (mEditor = new ScriptEdit (this, mDocument)); - - mEditor->setAcceptRichText (false); - mEditor->setLineWrapMode (QTextEdit::NoWrap); - mEditor->setTabStopWidth (4); - mEditor->setUndoRedoEnabled (false); // we use OpenCS-wide undo/redo instead + setWidget (mEditor = new ScriptEdit (mDocument, ScriptHighlighter::Mode_General, this)); mModel = &dynamic_cast ( *document.getData().getTableModel (CSMWorld::UniversalId::Type_Scripts)); @@ -58,14 +40,6 @@ CSVWorld::ScriptSubView::ScriptSubView (const CSMWorld::UniversalId& id, CSMDoc: connect (mModel, SIGNAL (rowsAboutToBeRemoved (const QModelIndex&, int, int)), this, SLOT (rowsAboutToBeRemoved (const QModelIndex&, int, int))); - - connect (&document.getData(), SIGNAL (idListChanged()), this, SLOT (idListChanged())); - - mHighlighter = new ScriptHighlighter (document.getData(), mEditor->document()); - - connect (&mUpdateTimer, SIGNAL (timeout()), this, SLOT (updateHighlighting())); - - mUpdateTimer.setSingleShot (true); } void CSVWorld::ScriptSubView::setEditLock (bool locked) @@ -73,20 +47,12 @@ void CSVWorld::ScriptSubView::setEditLock (bool locked) mEditor->setReadOnly (locked); } -void CSVWorld::ScriptSubView::idListChanged() -{ - mHighlighter->invalidateIds(); - - if (!mUpdateTimer.isActive()) - mUpdateTimer.start (0); -} - void CSVWorld::ScriptSubView::textChanged() { - if (mChangeLocked) + if (mEditor->isChangeLocked()) return; - ChangeLock lock (*this); + ScriptEdit::ChangeLock lock (*mEditor); mDocument.getUndoStack().push (new CSMWorld::ModifyCommand (*mModel, mModel->getModelIndex (getUniversalId().getId(), mColumn), mEditor->toPlainText())); @@ -94,10 +60,10 @@ void CSVWorld::ScriptSubView::textChanged() void CSVWorld::ScriptSubView::dataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) { - if (mChangeLocked) + if (mEditor->isChangeLocked()) return; - ChangeLock lock (*this); + ScriptEdit::ChangeLock lock (*mEditor); QModelIndex index = mModel->getModelIndex (getUniversalId().getId(), mColumn); @@ -115,15 +81,6 @@ void CSVWorld::ScriptSubView::rowsAboutToBeRemoved (const QModelIndex& parent, i QModelIndex index = mModel->getModelIndex (getUniversalId().getId(), mColumn); if (!parent.isValid() && index.row()>=start && index.row()<=end) - deleteLater(); + emit closeRequest(); } -void CSVWorld::ScriptSubView::updateHighlighting() -{ - if (mChangeLocked) - return; - - ChangeLock lock (*this); - - mHighlighter->rehighlight(); -} \ No newline at end of file diff --git a/apps/opencs/view/world/scriptsubview.hpp b/apps/opencs/view/world/scriptsubview.hpp index 7ceab70ba..77127d9be 100644 --- a/apps/opencs/view/world/scriptsubview.hpp +++ b/apps/opencs/view/world/scriptsubview.hpp @@ -3,9 +3,6 @@ #include "../doc/subview.hpp" -#include - -class QTextEdit; class QModelIndex; namespace CSMDoc @@ -20,34 +17,16 @@ namespace CSMWorld namespace CSVWorld { - class ScriptHighlighter; + class ScriptEdit; class ScriptSubView : public CSVDoc::SubView { Q_OBJECT - QTextEdit *mEditor; + ScriptEdit *mEditor; CSMDoc::Document& mDocument; CSMWorld::IdTable *mModel; int mColumn; - int mChangeLocked; - ScriptHighlighter *mHighlighter; - QTimer mUpdateTimer; - - class ChangeLock - { - ScriptSubView& mView; - - ChangeLock (const ChangeLock&); - ChangeLock& operator= (const ChangeLock&); - - public: - - ChangeLock (ScriptSubView& view); - ~ChangeLock(); - }; - - friend class ChangeLock; public: @@ -57,17 +36,11 @@ namespace CSVWorld public slots: - void idListChanged(); - void textChanged(); void dataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight); void rowsAboutToBeRemoved (const QModelIndex& parent, int start, int end); - - private slots: - - void updateHighlighting(); }; } diff --git a/apps/opencs/view/world/subviews.cpp b/apps/opencs/view/world/subviews.cpp index 75f391699..c5d969d0d 100644 --- a/apps/opencs/view/world/subviews.cpp +++ b/apps/opencs/view/world/subviews.cpp @@ -3,8 +3,6 @@ #include "../doc/subviewfactoryimp.hpp" -#include "../filter/filtercreator.hpp" - #include "tablesubview.hpp" #include "dialoguesubview.hpp" #include "scriptsubview.hpp" @@ -28,6 +26,9 @@ void CSVWorld::addSubViewFactories (CSVDoc::SubViewFactoryManager& manager) manager.add (CSMWorld::UniversalId::Type_Skills, new CSVDoc::SubViewFactoryWithCreator); + manager.add (CSMWorld::UniversalId::Type_MagicEffects, + new CSVDoc::SubViewFactoryWithCreator); + static const CSMWorld::UniversalId::Type sTableTypes[] = { CSMWorld::UniversalId::Type_Globals, @@ -35,10 +36,13 @@ void CSVWorld::addSubViewFactories (CSVDoc::SubViewFactoryManager& manager) CSMWorld::UniversalId::Type_Factions, CSMWorld::UniversalId::Type_Races, CSMWorld::UniversalId::Type_Sounds, - CSMWorld::UniversalId::Type_Scripts, CSMWorld::UniversalId::Type_Regions, CSMWorld::UniversalId::Type_Birthsigns, CSMWorld::UniversalId::Type_Spells, + CSMWorld::UniversalId::Type_Enchantments, + CSMWorld::UniversalId::Type_BodyParts, + CSMWorld::UniversalId::Type_SoundGens, + CSMWorld::UniversalId::Type_Pathgrids, CSMWorld::UniversalId::Type_None // end marker }; @@ -68,42 +72,77 @@ void CSVWorld::addSubViewFactories (CSVDoc::SubViewFactoryManager& manager) manager.add (CSMWorld::UniversalId::Type_JournalInfos, new CSVDoc::SubViewFactoryWithCreator > (false)); + // Subviews for resources tables + manager.add (CSMWorld::UniversalId::Type_Meshes, + new CSVDoc::SubViewFactoryWithCreator); + manager.add (CSMWorld::UniversalId::Type_Icons, + new CSVDoc::SubViewFactoryWithCreator); + manager.add (CSMWorld::UniversalId::Type_Musics, + new CSVDoc::SubViewFactoryWithCreator); + manager.add (CSMWorld::UniversalId::Type_SoundsRes, + new CSVDoc::SubViewFactoryWithCreator); + manager.add (CSMWorld::UniversalId::Type_Textures, + new CSVDoc::SubViewFactoryWithCreator); + manager.add (CSMWorld::UniversalId::Type_Videos, + new CSVDoc::SubViewFactoryWithCreator); + + // Subviews for editing/viewing individual records manager.add (CSMWorld::UniversalId::Type_Script, new CSVDoc::SubViewFactory); // Other stuff (combined record tables) manager.add (CSMWorld::UniversalId::Type_RegionMap, new CSVDoc::SubViewFactory); + manager.add (CSMWorld::UniversalId::Type_Scene, new CSVDoc::SubViewFactory); + + // More other stuff manager.add (CSMWorld::UniversalId::Type_Filters, new CSVDoc::SubViewFactoryWithCreator >); + CreatorFactory >); - manager.add (CSMWorld::UniversalId::Type_Scene, new CSVDoc::SubViewFactory); + manager.add (CSMWorld::UniversalId::Type_DebugProfiles, + new CSVDoc::SubViewFactoryWithCreator >); - //edit subviews - manager.add (CSMWorld::UniversalId::Type_Region, - new CSVDoc::SubViewFactoryWithCreator > (false)); + manager.add (CSMWorld::UniversalId::Type_Scripts, + new CSVDoc::SubViewFactoryWithCreator >); - manager.add (CSMWorld::UniversalId::Type_Spell, - new CSVDoc::SubViewFactoryWithCreator > (false)); + // Dialogue subviews + static const CSMWorld::UniversalId::Type sTableTypes2[] = + { + CSMWorld::UniversalId::Type_Region, + CSMWorld::UniversalId::Type_Spell, + CSMWorld::UniversalId::Type_Birthsign, + CSMWorld::UniversalId::Type_Global, + CSMWorld::UniversalId::Type_Race, + CSMWorld::UniversalId::Type_Class, + CSMWorld::UniversalId::Type_Sound, + CSMWorld::UniversalId::Type_Faction, + CSMWorld::UniversalId::Type_Enchantment, + CSMWorld::UniversalId::Type_BodyPart, + CSMWorld::UniversalId::Type_SoundGen, + CSMWorld::UniversalId::Type_Pathgrid, - manager.add (CSMWorld::UniversalId::Type_Referenceable, - new CSVDoc::SubViewFactoryWithCreator > (false)); + CSMWorld::UniversalId::Type_None // end marker + }; - manager.add (CSMWorld::UniversalId::Type_Birthsign, - new CSVDoc::SubViewFactoryWithCreator > (false)); + for (int i=0; sTableTypes2[i]!=CSMWorld::UniversalId::Type_None; ++i) + manager.add (sTableTypes2[i], + new CSVDoc::SubViewFactoryWithCreator > (false)); - manager.add (CSMWorld::UniversalId::Type_Global, - new CSVDoc::SubViewFactoryWithCreator > (false)); + manager.add (CSMWorld::UniversalId::Type_Skill, + new CSVDoc::SubViewFactoryWithCreator (false)); - manager.add (CSMWorld::UniversalId::Type_Gmst, - new CSVDoc::SubViewFactoryWithCreator > (false)); + manager.add (CSMWorld::UniversalId::Type_MagicEffect, + new CSVDoc::SubViewFactoryWithCreator (false)); - manager.add (CSMWorld::UniversalId::Type_Race, - new CSVDoc::SubViewFactoryWithCreator > (false)); + manager.add (CSMWorld::UniversalId::Type_Gmst, + new CSVDoc::SubViewFactoryWithCreator (false)); - manager.add (CSMWorld::UniversalId::Type_Class, - new CSVDoc::SubViewFactoryWithCreator > (false)); + manager.add (CSMWorld::UniversalId::Type_Referenceable, + new CSVDoc::SubViewFactoryWithCreator > (false)); manager.add (CSMWorld::UniversalId::Type_Reference, new CSVDoc::SubViewFactoryWithCreator > (false)); @@ -111,18 +150,6 @@ void CSVWorld::addSubViewFactories (CSVDoc::SubViewFactoryManager& manager) manager.add (CSMWorld::UniversalId::Type_Cell, new CSVDoc::SubViewFactoryWithCreator > (false)); - manager.add (CSMWorld::UniversalId::Type_Filter, - new CSVDoc::SubViewFactoryWithCreator > (false)); - - manager.add (CSMWorld::UniversalId::Type_Sound, - new CSVDoc::SubViewFactoryWithCreator > (false)); - - manager.add (CSMWorld::UniversalId::Type_Faction, - new CSVDoc::SubViewFactoryWithCreator > (false)); - - manager.add (CSMWorld::UniversalId::Type_Skill, - new CSVDoc::SubViewFactoryWithCreator > (false)); - manager.add (CSMWorld::UniversalId::Type_JournalInfo, new CSVDoc::SubViewFactoryWithCreator > (false)); @@ -135,6 +162,12 @@ void CSVWorld::addSubViewFactories (CSVDoc::SubViewFactoryManager& manager) manager.add (CSMWorld::UniversalId::Type_Journal, new CSVDoc::SubViewFactoryWithCreator (false)); + manager.add (CSMWorld::UniversalId::Type_DebugProfile, + new CSVDoc::SubViewFactoryWithCreator > (false)); + + manager.add (CSMWorld::UniversalId::Type_Filter, + new CSVDoc::SubViewFactoryWithCreator > (false)); + //preview manager.add (CSMWorld::UniversalId::Type_Preview, new CSVDoc::SubViewFactory); } \ No newline at end of file diff --git a/apps/opencs/view/world/table.cpp b/apps/opencs/view/world/table.cpp index 3d4b02c9c..e864e4ed2 100644 --- a/apps/opencs/view/world/table.cpp +++ b/apps/opencs/view/world/table.cpp @@ -14,24 +14,76 @@ #include "../../model/world/data.hpp" #include "../../model/world/commands.hpp" #include "../../model/world/idtableproxymodel.hpp" +#include "../../model/world/idtablebase.hpp" #include "../../model/world/idtable.hpp" #include "../../model/world/record.hpp" #include "../../model/world/columns.hpp" #include "../../model/world/tablemimedata.hpp" #include "../../model/world/tablemimedata.hpp" +#include "../../model/world/commanddispatcher.hpp" #include "recordstatusdelegate.hpp" #include "util.hpp" void CSVWorld::Table::contextMenuEvent (QContextMenuEvent *event) { + // configure dispatcher QModelIndexList selectedRows = selectionModel()->selectedRows(); + std::vector records; + + int columnIndex = mModel->findColumnIndex (CSMWorld::Columns::ColumnId_Id); + + for (QModelIndexList::const_iterator iter (selectedRows.begin()); iter!=selectedRows.end(); + ++iter) + { + int row = mProxyModel->mapToSource (mProxyModel->index (iter->row(), 0)).row(); + + records.push_back (mModel->data ( + mModel->index (row, columnIndex)).toString().toUtf8().constData()); + } + + mDispatcher->setSelection (records); + + std::vector extendedTypes = mDispatcher->getExtendedTypes(); + + mDispatcher->setExtendedTypes (extendedTypes); + + // create context menu QMenu menu (this); /// \todo add menu items for select all and clear selection - if (!mEditLock) + { + // Request UniversalId editing from table columns. + + int currRow = rowAt( event->y() ), + currCol = columnAt( event->x() ); + + currRow = mProxyModel->mapToSource(mProxyModel->index( currRow, 0 )).row(); + + CSMWorld::ColumnBase::Display colDisplay = + static_cast( + mModel->headerData( + currCol, + Qt::Horizontal, + CSMWorld::ColumnBase::Role_Display ).toInt()); + + QString cellData = mModel->data(mModel->index( currRow, currCol )).toString(); + CSMWorld::UniversalId::Type colType = CSMWorld::TableMimeData::convertEnums( colDisplay ); + + if ( !cellData.isEmpty() + && colType != CSMWorld::UniversalId::Type_None ) + { + mEditCellAction->setText(tr("Edit '").append(cellData).append("'")); + + menu.addAction( mEditCellAction ); + + mEditCellId = CSMWorld::UniversalId( colType, cellData.toUtf8().constData() ); + } + } + + if (!mEditLock && !(mModel->getFeatures() & CSMWorld::IdTableBase::Feature_Constant)) { if (selectedRows.size()==1) { @@ -44,22 +96,27 @@ void CSVWorld::Table::contextMenuEvent (QContextMenuEvent *event) if (mCreateAction) menu.addAction (mCreateAction); - /// \todo Reverting temporarily disabled on tables that support reordering, because - /// revert logic currently can not handle reordering. - if (mModel->getReordering()==CSMWorld::IdTable::Reordering_None) - if (listRevertableSelectedIds().size()>0) - menu.addAction (mRevertAction); + if (mDispatcher->canRevert()) + { + menu.addAction (mRevertAction); - if (listDeletableSelectedIds().size()>0) + if (!extendedTypes.empty()) + menu.addAction (mExtendedRevertAction); + } + + if (mDispatcher->canDelete()) + { menu.addAction (mDeleteAction); - if (mModel->getReordering()==CSMWorld::IdTable::Reordering_WithinTopic) + if (!extendedTypes.empty()) + menu.addAction (mExtendedDeleteAction); + } + + if (mModel->getFeatures() & CSMWorld::IdTableBase::Feature_ReorderWithinTopic) { /// \todo allow reordering of multiple rows if (selectedRows.size()==1) { - int row =selectedRows.begin()->row(); - int column = mModel->searchColumnIndex (CSMWorld::Columns::ColumnId_Topic); if (column==-1) @@ -67,14 +124,17 @@ void CSVWorld::Table::contextMenuEvent (QContextMenuEvent *event) if (column!=-1) { - if (row>0 && mProxyModel->data (mProxyModel->index (row, column))== - mProxyModel->data (mProxyModel->index (row-1, column))) + int row = mProxyModel->mapToSource ( + mProxyModel->index (selectedRows.begin()->row(), 0)).row(); + + if (row>0 && mModel->data (mModel->index (row, column))== + mModel->data (mModel->index (row-1, column))) { menu.addAction (mMoveUpAction); } - if (rowrowCount()-1 && mProxyModel->data (mProxyModel->index (row, column))== - mProxyModel->data (mProxyModel->index (row+1, column))) + if (rowrowCount()-1 && mModel->data (mModel->index (row, column))== + mModel->data (mModel->index (row+1, column))) { menu.addAction (mMoveDownAction); } @@ -85,12 +145,12 @@ void CSVWorld::Table::contextMenuEvent (QContextMenuEvent *event) if (selectedRows.size()==1) { - if (mModel->getViewing()!=CSMWorld::IdTable::Viewing_None) - { - int row = selectedRows.begin()->row(); + int row = selectedRows.begin()->row(); - row = mProxyModel->mapToSource (mProxyModel->index (row, 0)).row(); + row = mProxyModel->mapToSource (mProxyModel->index (row, 0)).row(); + if (mModel->getFeatures() & CSMWorld::IdTableBase::Feature_View) + { CSMWorld::UniversalId id = mModel->view (row).first; int index = mDocument.getData().getCells().searchId (id.getId()); @@ -101,89 +161,93 @@ void CSVWorld::Table::contextMenuEvent (QContextMenuEvent *event) menu.addAction (mViewAction); } - if (mModel->hasPreview()) - menu.addAction (mPreviewAction); + if (mModel->getFeatures() & CSMWorld::IdTableBase::Feature_Preview) + { + QModelIndex index = mModel->index (row, + mModel->findColumnIndex (CSMWorld::Columns::ColumnId_Modification)); + + CSMWorld::RecordBase::State state = static_cast ( + mModel->data (index).toInt()); + + if (state!=CSMWorld::RecordBase::State_Deleted) + menu.addAction (mPreviewAction); + } } menu.exec (event->globalPos()); } -std::vector CSVWorld::Table::listRevertableSelectedIds() const +void CSVWorld::Table::mouseDoubleClickEvent (QMouseEvent *event) { - std::vector revertableIds; + Qt::KeyboardModifiers modifiers = + event->modifiers() & (Qt::ShiftModifier | Qt::ControlModifier); - if (mProxyModel->columnCount()>0) - { - QModelIndexList selectedRows = selectionModel()->selectedRows(); + QModelIndex index = currentIndex(); - for (QModelIndexList::const_iterator iter (selectedRows.begin()); iter!=selectedRows.end(); - ++iter) - { - QModelIndex index = mProxyModel->mapToSource (mProxyModel->index (iter->row(), 0)); + selectionModel()->select (index, + QItemSelectionModel::Clear | QItemSelectionModel::Select | QItemSelectionModel::Rows); - CSMWorld::RecordBase::State state = - static_cast ( - mModel->data (mModel->index (index.row(), 1)).toInt()); + std::map::iterator iter = + mDoubleClickActions.find (modifiers); - if (state!=CSMWorld::RecordBase::State_BaseOnly) - { - int columnIndex = mModel->findColumnIndex (CSMWorld::Columns::ColumnId_Id); + if (iter==mDoubleClickActions.end()) + { + event->accept(); + return; + } - std::string id = mModel->data (mModel->index (index.row(), columnIndex)). - toString().toUtf8().constData(); + switch (iter->second) + { + case Action_None: - revertableIds.push_back (id); - } - } - } + event->accept(); + break; - return revertableIds; -} + case Action_InPlaceEdit: -std::vector CSVWorld::Table::listDeletableSelectedIds() const -{ - std::vector deletableIds; + DragRecordTable::mouseDoubleClickEvent (event); + break; - if (mProxyModel->columnCount()>0) - { - QModelIndexList selectedRows = selectionModel()->selectedRows(); + case Action_EditRecord: - for (QModelIndexList::const_iterator iter (selectedRows.begin()); iter!=selectedRows.end(); - ++iter) - { - QModelIndex index = mProxyModel->mapToSource (mProxyModel->index (iter->row(), 0)); + event->accept(); + editRecord(); + break; - // check record state - CSMWorld::RecordBase::State state = - static_cast ( - mModel->data (mModel->index (index.row(), 1)).toInt()); + case Action_View: - if (state==CSMWorld::RecordBase::State_Deleted) - continue; + event->accept(); + viewRecord(); + break; - // check other columns (only relevant for a subset of the tables) - int dialogueTypeIndex = - mModel->searchColumnIndex (CSMWorld::Columns::ColumnId_DialogueType); + case Action_Revert: - if (dialogueTypeIndex!=-1) - { - int type = mModel->data (mModel->index (index.row(), dialogueTypeIndex)).toInt(); + event->accept(); + if (mDispatcher->canRevert()) + mDispatcher->executeRevert(); + break; - if (type!=ESM::Dialogue::Topic && type!=ESM::Dialogue::Journal) - continue; - } + case Action_Delete: - // add the id to the collection - int columnIndex = mModel->findColumnIndex (CSMWorld::Columns::ColumnId_Id); + event->accept(); + if (mDispatcher->canDelete()) + mDispatcher->executeDelete(); + break; - std::string id = mModel->data (mModel->index (index.row(), columnIndex)). - toString().toUtf8().constData(); + case Action_EditRecordAndClose: - deletableIds.push_back (id); - } - } + event->accept(); + editRecord(); + emit closeRequest(); + break; + + case Action_ViewAndClose: - return deletableIds; + event->accept(); + viewRecord(); + emit closeRequest(); + break; + } } CSVWorld::Table::Table (const CSMWorld::UniversalId& id, @@ -191,11 +255,13 @@ CSVWorld::Table::Table (const CSMWorld::UniversalId& id, : mCreateAction (0), mCloneAction(0), mRecordStatusDisplay (0), DragRecordTable(document) { - mModel = &dynamic_cast (*mDocument.getData().getTableModel (id)); + mModel = &dynamic_cast (*mDocument.getData().getTableModel (id)); mProxyModel = new CSMWorld::IdTableProxyModel (this); mProxyModel->setSourceModel (mModel); + mDispatcher = new CSMWorld::CommandDispatcher (document, id, this); + setModel (mProxyModel); horizontalHeader()->setResizeMode (QHeaderView::Interactive); verticalHeader()->hide(); @@ -215,7 +281,7 @@ CSVWorld::Table::Table (const CSMWorld::UniversalId& id, mModel->headerData (i, Qt::Horizontal, CSMWorld::ColumnBase::Role_Display).toInt()); CommandDelegate *delegate = CommandDelegateFactoryCollection::get().makeDelegate (display, - mDocument.getUndoStack(), this); + mDocument, this); mDelegates.push_back (delegate); setItemDelegateForColumn (i, delegate); @@ -240,11 +306,11 @@ CSVWorld::Table::Table (const CSMWorld::UniversalId& id, } mRevertAction = new QAction (tr ("Revert Record"), this); - connect (mRevertAction, SIGNAL (triggered()), this, SLOT (revertRecord())); + connect (mRevertAction, SIGNAL (triggered()), mDispatcher, SLOT (executeRevert())); addAction (mRevertAction); mDeleteAction = new QAction (tr ("Delete Record"), this); - connect (mDeleteAction, SIGNAL (triggered()), this, SLOT (deleteRecord())); + connect (mDeleteAction, SIGNAL (triggered()), mDispatcher, SLOT (executeDelete())); addAction (mDeleteAction); mMoveUpAction = new QAction (tr ("Move Up"), this); @@ -255,6 +321,10 @@ CSVWorld::Table::Table (const CSMWorld::UniversalId& id, connect (mMoveDownAction, SIGNAL (triggered()), this, SLOT (moveDownRecord())); addAction (mMoveDownAction); + mEditCellAction = new QAction( tr("Edit Cell"), this ); + connect( mEditCellAction, SIGNAL(triggered()), this, SLOT(editCell()) ); + addAction( mEditCellAction ); + mViewAction = new QAction (tr ("View"), this); connect (mViewAction, SIGNAL (triggered()), this, SLOT (viewRecord())); addAction (mViewAction); @@ -263,6 +333,18 @@ CSVWorld::Table::Table (const CSMWorld::UniversalId& id, connect (mPreviewAction, SIGNAL (triggered()), this, SLOT (previewRecord())); addAction (mPreviewAction); + /// \todo add a user option, that redirects the extended action to an input panel (in + /// the bottom bar) that lets the user select which record collections should be + /// modified. + + mExtendedDeleteAction = new QAction (tr ("Extended Delete Record"), this); + connect (mExtendedDeleteAction, SIGNAL (triggered()), mDispatcher, SLOT (executeExtendedDelete())); + addAction (mExtendedDeleteAction); + + mExtendedRevertAction = new QAction (tr ("Extended Revert Record"), this); + connect (mExtendedRevertAction, SIGNAL (triggered()), mDispatcher, SLOT (executeExtendedRevert())); + addAction (mExtendedRevertAction); + connect (mProxyModel, SIGNAL (rowsInserted (const QModelIndex&, int, int)), this, SLOT (tableSizeUpdate())); @@ -275,6 +357,11 @@ CSVWorld::Table::Table (const CSMWorld::UniversalId& id, this, SLOT (selectionSizeUpdate ())); setAcceptDrops(true); + + mDoubleClickActions.insert (std::make_pair (Qt::NoModifier, Action_InPlaceEdit)); + mDoubleClickActions.insert (std::make_pair (Qt::ShiftModifier, Action_EditRecord)); + mDoubleClickActions.insert (std::make_pair (Qt::ControlModifier, Action_View)); + mDoubleClickActions.insert (std::make_pair (Qt::ShiftModifier | Qt::ControlModifier, Action_EditRecordAndClose)); } void CSVWorld::Table::setEditLock (bool locked) @@ -283,58 +370,24 @@ void CSVWorld::Table::setEditLock (bool locked) (*iter)->setEditLock (locked); DragRecordTable::setEditLock(locked); + mDispatcher->setEditLock (locked); } CSMWorld::UniversalId CSVWorld::Table::getUniversalId (int row) const { - return CSMWorld::UniversalId ( - static_cast (mProxyModel->data (mProxyModel->index (row, 2)).toInt()), - mProxyModel->data (mProxyModel->index (row, 0)).toString().toUtf8().constData()); -} + row = mProxyModel->mapToSource (mProxyModel->index (row, 0)).row(); -void CSVWorld::Table::revertRecord() -{ - if (!mEditLock) - { - std::vector revertableIds = listRevertableSelectedIds(); - - if (!revertableIds.empty()) - { - if (revertableIds.size()>1) - mDocument.getUndoStack().beginMacro (tr ("Revert multiple records")); + int idColumn = mModel->findColumnIndex (CSMWorld::Columns::ColumnId_Id); + int typeColumn = mModel->findColumnIndex (CSMWorld::Columns::ColumnId_RecordType); - for (std::vector::const_iterator iter (revertableIds.begin()); iter!=revertableIds.end(); ++iter) - mDocument.getUndoStack().push (new CSMWorld::RevertCommand (*mModel, *iter)); - - if (revertableIds.size()>1) - mDocument.getUndoStack().endMacro(); - } - } -} - -void CSVWorld::Table::deleteRecord() -{ - if (!mEditLock) - { - std::vector deletableIds = listDeletableSelectedIds(); - - if (!deletableIds.empty()) - { - if (deletableIds.size()>1) - mDocument.getUndoStack().beginMacro (tr ("Delete multiple records")); - - for (std::vector::const_iterator iter (deletableIds.begin()); iter!=deletableIds.end(); ++iter) - mDocument.getUndoStack().push (new CSMWorld::DeleteCommand (*mModel, *iter)); - - if (deletableIds.size()>1) - mDocument.getUndoStack().endMacro(); - } - } + return CSMWorld::UniversalId ( + static_cast (mModel->data (mModel->index (row, typeColumn)).toInt()), + mModel->data (mModel->index (row, idColumn)).toString().toUtf8().constData()); } void CSVWorld::Table::editRecord() { - if (!mEditLock) + if (!mEditLock || (mModel->getFeatures() & CSMWorld::IdTableBase::Feature_Constant)) { QModelIndexList selectedRows = selectionModel()->selectedRows(); @@ -345,11 +398,11 @@ void CSVWorld::Table::editRecord() void CSVWorld::Table::cloneRecord() { - if (!mEditLock) + if (!mEditLock || (mModel->getFeatures() & CSMWorld::IdTableBase::Feature_Constant)) { QModelIndexList selectedRows = selectionModel()->selectedRows(); const CSMWorld::UniversalId& toClone = getUniversalId(selectedRows.begin()->row()); - if (selectedRows.size()==1 && !mModel->getRecord(toClone.getId()).isDeleted()) + if (selectedRows.size()==1 && !mModel->isDeleted (toClone.getId())) { emit cloneRequest (toClone); } @@ -358,7 +411,7 @@ void CSVWorld::Table::cloneRecord() void CSVWorld::Table::moveUpRecord() { - if (mEditLock) + if (mEditLock || (mModel->getFeatures() & CSMWorld::IdTableBase::Feature_Constant)) return; QModelIndexList selectedRows = selectionModel()->selectedRows(); @@ -383,14 +436,15 @@ void CSVWorld::Table::moveUpRecord() for (int i=1; i (*mModel), row, newOrder)); } } } void CSVWorld::Table::moveDownRecord() { - if (mEditLock) + if (mEditLock || (mModel->getFeatures() & CSMWorld::IdTableBase::Feature_Constant)) return; QModelIndexList selectedRows = selectionModel()->selectedRows(); @@ -415,13 +469,22 @@ void CSVWorld::Table::moveDownRecord() for (int i=1; i (*mModel), row, newOrder)); } } } +void CSVWorld::Table::editCell() +{ + emit editRequest( mEditCellId, std::string() ); +} + void CSVWorld::Table::viewRecord() { + if (!(mModel->getFeatures() & CSMWorld::IdTableBase::Feature_View)) + return; + QModelIndexList selectedRows = selectionModel()->selectedRows(); if (selectedRows.size()==1) @@ -445,25 +508,71 @@ void CSVWorld::Table::previewRecord() { std::string id = getUniversalId (selectedRows.begin()->row()).getId(); - emit editRequest (CSMWorld::UniversalId (CSMWorld::UniversalId::Type_Preview, id) , ""); + QModelIndex index = mModel->getModelIndex (id, + mModel->findColumnIndex (CSMWorld::Columns::ColumnId_Modification)); + + if (mModel->data (index)!=CSMWorld::RecordBase::State_Deleted) + emit editRequest (CSMWorld::UniversalId (CSMWorld::UniversalId::Type_Preview, id), + ""); } } void CSVWorld::Table::updateUserSetting (const QString &name, const QStringList &list) { - int columns = mModel->columnCount(); + if (name=="records/type-format" || name=="records/status-format") + { + int columns = mModel->columnCount(); - for (int i=0; i - (*delegate).updateUserSetting (name, list); + for (int i=0; iindex (0, i), - mModel->index (mModel->rowCount()-1, i)); + dynamic_cast + (*delegate).updateUserSetting (name, list); + { + emit dataChanged (mModel->index (0, i), + mModel->index (mModel->rowCount()-1, i)); + } } - } + return; + } + + QString base ("table-input/double"); + if (name.startsWith (base)) + { + QString modifierString = name.mid (base.size()); + Qt::KeyboardModifiers modifiers = 0; + + if (modifierString=="-s") + modifiers = Qt::ShiftModifier; + else if (modifierString=="-c") + modifiers = Qt::ControlModifier; + else if (modifierString=="-sc") + modifiers = Qt::ShiftModifier | Qt::ControlModifier; + + DoubleClickAction action = Action_None; + + QString value = list.at (0); + + if (value=="Edit in Place") + action = Action_InPlaceEdit; + else if (value=="Edit Record") + action = Action_EditRecord; + else if (value=="View") + action = Action_View; + else if (value=="Revert") + action = Action_Revert; + else if (value=="Delete") + action = Action_Delete; + else if (value=="Edit Record and Close") + action = Action_EditRecordAndClose; + else if (value=="View and Close") + action = Action_ViewAndClose; + + mDoubleClickActions[modifiers] = action; + + return; + } } void CSVWorld::Table::tableSizeUpdate() @@ -476,29 +585,35 @@ void CSVWorld::Table::tableSizeUpdate() { int rows = mProxyModel->rowCount(); - for (int i=0; isearchColumnIndex (CSMWorld::Columns::ColumnId_Modification); + + if (columnIndex!=-1) { - QModelIndex index = mProxyModel->mapToSource (mProxyModel->index (i, 0)); + for (int i=0; imapToSource (mProxyModel->index (i, 0)); - int columnIndex = mModel->findColumnIndex (CSMWorld::Columns::ColumnId_Modification); - int state = mModel->data (mModel->index (index.row(), columnIndex)).toInt(); + int state = mModel->data (mModel->index (index.row(), columnIndex)).toInt(); - switch (state) - { - case CSMWorld::RecordBase::State_BaseOnly: ++size; break; - case CSMWorld::RecordBase::State_Modified: ++size; ++modified; break; - case CSMWorld::RecordBase::State_ModifiedOnly: ++size; ++modified; break; - case CSMWorld::RecordBase:: State_Deleted: ++deleted; ++modified; break; + switch (state) + { + case CSMWorld::RecordBase::State_BaseOnly: ++size; break; + case CSMWorld::RecordBase::State_Modified: ++size; ++modified; break; + case CSMWorld::RecordBase::State_ModifiedOnly: ++size; ++modified; break; + case CSMWorld::RecordBase:: State_Deleted: ++deleted; ++modified; break; + } } } + else + size = rows; } - tableSizeChanged (size, deleted, modified); + emit tableSizeChanged (size, deleted, modified); } void CSVWorld::Table::selectionSizeUpdate() { - selectionSizeChanged (selectionModel()->selectedRows().size()); + emit selectionSizeChanged (selectionModel()->selectedRows().size()); } void CSVWorld::Table::requestFocus (const std::string& id) @@ -512,6 +627,8 @@ void CSVWorld::Table::requestFocus (const std::string& id) void CSVWorld::Table::recordFilterChanged (boost::shared_ptr filter) { mProxyModel->setFilter (filter); + tableSizeUpdate(); + selectionSizeUpdate(); } void CSVWorld::Table::mouseMoveEvent (QMouseEvent* event) @@ -532,6 +649,9 @@ void CSVWorld::Table::dropEvent(QDropEvent *event) } const CSMWorld::TableMimeData* mime = dynamic_cast (event->mimeData()); + if (!mime) // May happen when non-records (e.g. plain text) are dragged and dropped + return; + if (mime->fromDocument (mDocument)) { CSMWorld::ColumnBase::Display display = static_cast @@ -569,7 +689,6 @@ std::vector CSVWorld::Table::getColumnsWithDisplay(CSMWorld::Column std::vector< CSMWorld::UniversalId > CSVWorld::Table::getDraggedRecords() const { - QModelIndexList selectedRows = selectionModel()->selectedRows(); std::vector idToDrag; diff --git a/apps/opencs/view/world/table.hpp b/apps/opencs/view/world/table.hpp index 3b1d40e78..75161b8b6 100644 --- a/apps/opencs/view/world/table.hpp +++ b/apps/opencs/view/world/table.hpp @@ -8,6 +8,7 @@ #include "../../model/filter/node.hpp" #include "../../model/world/columnbase.hpp" +#include "../../model/world/universalid.hpp" #include "dragrecordtable.hpp" class QUndoStack; @@ -21,9 +22,9 @@ namespace CSMDoc namespace CSMWorld { class Data; - class UniversalId; class IdTableProxyModel; - class IdTable; + class IdTableBase; + class CommandDispatcher; } namespace CSVWorld @@ -35,6 +36,18 @@ namespace CSVWorld { Q_OBJECT + enum DoubleClickAction + { + Action_None, + Action_InPlaceEdit, + Action_EditRecord, + Action_View, + Action_Revert, + Action_Delete, + Action_EditRecordAndClose, + Action_ViewAndClose + }; + std::vector mDelegates; QAction *mEditAction; QAction *mCreateAction; @@ -44,23 +57,29 @@ namespace CSVWorld QAction *mMoveUpAction; QAction *mMoveDownAction; QAction *mViewAction; + QAction *mEditCellAction; QAction *mPreviewAction; + QAction *mExtendedDeleteAction; + QAction *mExtendedRevertAction; CSMWorld::IdTableProxyModel *mProxyModel; - CSMWorld::IdTable *mModel; + CSMWorld::IdTableBase *mModel; int mRecordStatusDisplay; + CSMWorld::CommandDispatcher *mDispatcher; + CSMWorld::UniversalId mEditCellId; + std::map mDoubleClickActions; private: void contextMenuEvent (QContextMenuEvent *event); - std::vector listRevertableSelectedIds() const; - - std::vector listDeletableSelectedIds() const; - void mouseMoveEvent(QMouseEvent *event); void dropEvent(QDropEvent *event); + protected: + + virtual void mouseDoubleClickEvent (QMouseEvent *event); + public: Table (const CSMWorld::UniversalId& id, bool createAndDelete, @@ -91,11 +110,11 @@ namespace CSVWorld void cloneRequest(const CSMWorld::UniversalId&); - private slots: + void closeRequest(); - void revertRecord(); + private slots: - void deleteRecord(); + void editCell(); void editRecord(); diff --git a/apps/opencs/view/world/tablesubview.cpp b/apps/opencs/view/world/tablesubview.cpp index a0bef366b..54518023b 100644 --- a/apps/opencs/view/world/tablesubview.cpp +++ b/apps/opencs/view/world/tablesubview.cpp @@ -69,6 +69,8 @@ CSVWorld::TableSubView::TableSubView (const CSMWorld::UniversalId& id, CSMDoc::D connect(mFilterBox, SIGNAL(recordDropped(std::vector&, Qt::DropAction)), this, SLOT(createFilterRequest(std::vector&, Qt::DropAction))); + + connect (mTable, SIGNAL (closeRequest()), this, SLOT (closeRequest())); } void CSVWorld::TableSubView::setEditLock (bool locked) @@ -111,14 +113,21 @@ void CSVWorld::TableSubView::createFilterRequest (std::vector< CSMWorld::Univers { std::vector > > filterSource; + std::vector refIdColumns = mTable->getColumnsWithDisplay(CSMWorld::TableMimeData::convertEnums(CSMWorld::UniversalId::Type_Referenceable)); + bool hasRefIdDisplay = !refIdColumns.empty(); + for (std::vector::iterator it(types.begin()); it != types.end(); ++it) { - std::pair > pair( //splited long line - std::make_pair(it->getId(), mTable->getColumnsWithDisplay(CSMWorld::TableMimeData::convertEnums(it->getType())))); + CSMWorld::UniversalId::Type type = it->getType(); + std::vector col = mTable->getColumnsWithDisplay(CSMWorld::TableMimeData::convertEnums(type)); + if(!col.empty()) + { + filterSource.push_back(std::make_pair(it->getId(), col)); + } - if(!pair.second.empty()) + if(hasRefIdDisplay && CSMWorld::TableMimeData::isReferencable(type)) { - filterSource.push_back(pair); + filterSource.push_back(std::make_pair(it->getId(), refIdColumns)); } } @@ -131,6 +140,9 @@ bool CSVWorld::TableSubView::eventFilter (QObject* object, QEvent* event) { QDropEvent* drop = dynamic_cast(event); const CSMWorld::TableMimeData* data = dynamic_cast(drop->mimeData()); + if (!data) // May happen when non-records (e.g. plain text) are dragged and dropped + return false; + bool handled = data->holdsType(CSMWorld::UniversalId::Type_Filter); if (handled) { @@ -140,3 +152,4 @@ bool CSVWorld::TableSubView::eventFilter (QObject* object, QEvent* event) } return false; } + diff --git a/apps/opencs/view/world/util.cpp b/apps/opencs/view/world/util.cpp index ea8a7c541..f8847abfc 100644 --- a/apps/opencs/view/world/util.cpp +++ b/apps/opencs/view/world/util.cpp @@ -17,6 +17,8 @@ #include "../../model/world/commands.hpp" #include "../../model/world/tablemimedata.hpp" +#include "scriptedit.hpp" + CSVWorld::NastyTableModelHack::NastyTableModelHack (QAbstractItemModel& model) : mModel (model) {} @@ -78,15 +80,15 @@ void CSVWorld::CommandDelegateFactoryCollection::add (CSMWorld::ColumnBase::Disp } CSVWorld::CommandDelegate *CSVWorld::CommandDelegateFactoryCollection::makeDelegate ( - CSMWorld::ColumnBase::Display display, QUndoStack& undoStack, QObject *parent) const + CSMWorld::ColumnBase::Display display, CSMDoc::Document& document, QObject *parent) const { std::map::const_iterator iter = mFactories.find (display); if (iter!=mFactories.end()) - return iter->second->makeDelegate (undoStack, parent); + return iter->second->makeDelegate (document, parent); - return new CommandDelegate (undoStack, parent); + return new CommandDelegate (document, parent); } const CSVWorld::CommandDelegateFactoryCollection& CSVWorld::CommandDelegateFactoryCollection::get() @@ -100,7 +102,12 @@ const CSVWorld::CommandDelegateFactoryCollection& CSVWorld::CommandDelegateFacto QUndoStack& CSVWorld::CommandDelegate::getUndoStack() const { - return mUndoStack; + return mDocument.getUndoStack(); +} + +CSMDoc::Document& CSVWorld::CommandDelegate::getDocument() const +{ + return mDocument; } void CSVWorld::CommandDelegate::setModelDataImp (QWidget *editor, QAbstractItemModel *model, @@ -112,11 +119,11 @@ void CSVWorld::CommandDelegate::setModelDataImp (QWidget *editor, QAbstractItemM QVariant new_ = hack.getData(); if (model->data (index)!=new_) - mUndoStack.push (new CSMWorld::ModifyCommand (*model, index, new_)); + getUndoStack().push (new CSMWorld::ModifyCommand (*model, index, new_)); } -CSVWorld::CommandDelegate::CommandDelegate (QUndoStack& undoStack, QObject *parent) -: QStyledItemDelegate (parent), mUndoStack (undoStack), mEditLock (false) +CSVWorld::CommandDelegate::CommandDelegate (CSMDoc::Document& document, QObject *parent) +: QStyledItemDelegate (parent), mDocument (document), mEditLock (false) {} void CSVWorld::CommandDelegate::setModelData (QWidget *editor, QAbstractItemModel *model, @@ -143,48 +150,63 @@ QWidget *CSVWorld::CommandDelegate::createEditor (QWidget *parent, const QStyleO } } - if (display != CSMWorld::ColumnBase::Display_None) + switch (display) { - if (variant.type() == QVariant::Color) - { + case CSMWorld::ColumnBase::Display_Colour: + return new QLineEdit(parent); - } - if (display == CSMWorld::ColumnBase::Display_Integer) - { + + case CSMWorld::ColumnBase::Display_Integer: + return new QSpinBox(parent); - } - if (display == CSMWorld::ColumnBase::Display_Var) - { + + case CSMWorld::ColumnBase::Display_Var: + return new QLineEdit(parent); - } - if (display == CSMWorld::ColumnBase::Display_Float) - { + + case CSMWorld::ColumnBase::Display_Float: + return new QDoubleSpinBox(parent); - } - if (display == CSMWorld::ColumnBase::Display_LongString) + + case CSMWorld::ColumnBase::Display_LongString: { - return new QTextEdit(parent); + QPlainTextEdit *edit = new QPlainTextEdit(parent); + edit->setUndoRedoEnabled (false); + return edit; } - if (display == CSMWorld::ColumnBase::Display_String || - display == CSMWorld::ColumnBase::Display_Skill || - display == CSMWorld::ColumnBase::Display_Script || - display == CSMWorld::ColumnBase::Display_Race || - display == CSMWorld::ColumnBase::Display_Class || - display == CSMWorld::ColumnBase::Display_Faction || - display == CSMWorld::ColumnBase::Display_Miscellaneous || - display == CSMWorld::ColumnBase::Display_Sound) - { - return new DropLineEdit(parent); - } - if (display == CSMWorld::ColumnBase::Display_Boolean) - { + + case CSMWorld::ColumnBase::Display_Boolean: + return new QCheckBox(parent); - } - } - return QStyledItemDelegate::createEditor (parent, option, index); -} + case CSMWorld::ColumnBase::Display_String: + case CSMWorld::ColumnBase::Display_Skill: + case CSMWorld::ColumnBase::Display_Script: + case CSMWorld::ColumnBase::Display_Race: + case CSMWorld::ColumnBase::Display_Region: + case CSMWorld::ColumnBase::Display_Class: + case CSMWorld::ColumnBase::Display_Faction: + case CSMWorld::ColumnBase::Display_Miscellaneous: + case CSMWorld::ColumnBase::Display_Sound: + case CSMWorld::ColumnBase::Display_Mesh: + case CSMWorld::ColumnBase::Display_Icon: + case CSMWorld::ColumnBase::Display_Music: + case CSMWorld::ColumnBase::Display_SoundRes: + case CSMWorld::ColumnBase::Display_Texture: + case CSMWorld::ColumnBase::Display_Video: + case CSMWorld::ColumnBase::Display_GlobalVariable: + + return new DropLineEdit(parent); + + case CSMWorld::ColumnBase::Display_ScriptLines: + + return new ScriptEdit (mDocument, ScriptHighlighter::Mode_Console, parent); + default: + + return QStyledItemDelegate::createEditor (parent, option, index); + } +} void CSVWorld::CommandDelegate::setEditLock (bool locked) { @@ -255,6 +277,9 @@ void CSVWorld::DropLineEdit::dragMoveEvent(QDragMoveEvent *event) void CSVWorld::DropLineEdit::dropEvent(QDropEvent *event) { const CSMWorld::TableMimeData* data(dynamic_cast(event->mimeData())); + if (!data) // May happen when non-records (e.g. plain text) are dragged and dropped + return; + emit tableMimeDataDropped(data->getData(), data->getDocumentPtr()); //WIP } diff --git a/apps/opencs/view/world/util.hpp b/apps/opencs/view/world/util.hpp index 1c7e37818..b4d972bf3 100644 --- a/apps/opencs/view/world/util.hpp +++ b/apps/opencs/view/world/util.hpp @@ -51,7 +51,8 @@ namespace CSVWorld virtual ~CommandDelegateFactory(); - virtual CommandDelegate *makeDelegate (QUndoStack& undoStack, QObject *parent) const = 0; + virtual CommandDelegate *makeDelegate (CSMDoc::Document& document, QObject *parent) + const = 0; ///< The ownership of the returned CommandDelegate is transferred to the caller. }; @@ -73,11 +74,11 @@ namespace CSVWorld ~CommandDelegateFactoryCollection(); void add (CSMWorld::ColumnBase::Display display, CommandDelegateFactory *factory); - ///< The ownership of \Ʀ factory is transferred to *this. + ///< The ownership of \a factory is transferred to *this. /// - /// This function must not be called more than once per value of \Ʀ display. + /// This function must not be called more than once per value of \a display. - CommandDelegate *makeDelegate (CSMWorld::ColumnBase::Display display, QUndoStack& undoStack, + CommandDelegate *makeDelegate (CSMWorld::ColumnBase::Display display, CSMDoc::Document& document, QObject *parent) const; ///< The ownership of the returned CommandDelegate is transferred to the caller. /// @@ -110,19 +111,21 @@ namespace CSVWorld { Q_OBJECT - QUndoStack& mUndoStack; + CSMDoc::Document& mDocument; bool mEditLock; protected: QUndoStack& getUndoStack() const; + CSMDoc::Document& getDocument() const; + virtual void setModelDataImp (QWidget *editor, QAbstractItemModel *model, const QModelIndex& index) const; public: - CommandDelegate (QUndoStack& undoStack, QObject *parent); + CommandDelegate (CSMDoc::Document& document, QObject *parent); virtual void setModelData (QWidget *editor, QAbstractItemModel *model, const QModelIndex& index) const; diff --git a/apps/opencs/view/world/vartypedelegate.cpp b/apps/opencs/view/world/vartypedelegate.cpp index fc00f4491..c3c98b800 100644 --- a/apps/opencs/view/world/vartypedelegate.cpp +++ b/apps/opencs/view/world/vartypedelegate.cpp @@ -47,8 +47,8 @@ void CSVWorld::VarTypeDelegate::addCommands (QAbstractItemModel *model, const QM } CSVWorld::VarTypeDelegate::VarTypeDelegate (const std::vector >& values, - QUndoStack& undoStack, QObject *parent) -: EnumDelegate (values, undoStack, parent) + CSMDoc::Document& document, QObject *parent) +: EnumDelegate (values, document, parent) {} @@ -68,10 +68,10 @@ CSVWorld::VarTypeDelegateFactory::VarTypeDelegateFactory (ESM::VarType type0, add (type3); } -CSVWorld::CommandDelegate *CSVWorld::VarTypeDelegateFactory::makeDelegate (QUndoStack& undoStack, - QObject *parent) const +CSVWorld::CommandDelegate *CSVWorld::VarTypeDelegateFactory::makeDelegate ( + CSMDoc::Document& document, QObject *parent) const { - return new VarTypeDelegate (mValues, undoStack, parent); + return new VarTypeDelegate (mValues, document, parent); } void CSVWorld::VarTypeDelegateFactory::add (ESM::VarType type) diff --git a/apps/opencs/view/world/vartypedelegate.hpp b/apps/opencs/view/world/vartypedelegate.hpp index c8493f029..c86b936f6 100644 --- a/apps/opencs/view/world/vartypedelegate.hpp +++ b/apps/opencs/view/world/vartypedelegate.hpp @@ -17,7 +17,7 @@ namespace CSVWorld public: VarTypeDelegate (const std::vector >& values, - QUndoStack& undoStack, QObject *parent); + CSMDoc::Document& document, QObject *parent); }; class VarTypeDelegateFactory : public CommandDelegateFactory @@ -30,7 +30,7 @@ namespace CSVWorld ESM::VarType type1 = ESM::VT_Unknown, ESM::VarType type2 = ESM::VT_Unknown, ESM::VarType type3 = ESM::VT_Unknown); - virtual CommandDelegate *makeDelegate (QUndoStack& undoStack, QObject *parent) const; + virtual CommandDelegate *makeDelegate (CSMDoc::Document& document, QObject *parent) const; ///< The ownership of the returned CommandDelegate is transferred to the caller. void add (ESM::VarType type); diff --git a/apps/openmw/CMakeLists.txt b/apps/openmw/CMakeLists.txt index 8496b47a4..803c74325 100644 --- a/apps/openmw/CMakeLists.txt +++ b/apps/openmw/CMakeLists.txt @@ -2,8 +2,15 @@ set(GAME main.cpp engine.cpp + + ${CMAKE_SOURCE_DIR}/files/windows/openmw.rc ) -if(NOT WIN32) + +if (ANDROID) + set(GAME ${GAME} android_main.c) +endif() + +if(NOT WIN32 AND NOT ANDROID) set(GAME ${GAME} crashcatcher.cpp) endif() set(GAME_HEADER @@ -14,7 +21,7 @@ source_group(game FILES ${GAME} ${GAME_HEADER}) add_openmw_dir (mwrender renderingmanager debugging sky camera animation npcanimation creatureanimation activatoranimation actors objects renderinginterface localmap occlusionquery water shadows - characterpreview globalmap videoplayer ripplesimulation refraction + characterpreview globalmap ripplesimulation refraction terrainstorage renderconst effectmanager weaponanimation ) @@ -25,19 +32,19 @@ add_openmw_dir (mwinput add_openmw_dir (mwgui textinput widgets race class birth review windowmanagerimp console dialogue windowbase statswindow messagebox journalwindow charactercreation - mapwindow windowpinnablebase tooltips scrollwindow bookwindow list + mapwindow windowpinnablebase tooltips scrollwindow bookwindow formatting inventorywindow container hud countdialog tradewindow settingswindow confirmationdialog alchemywindow referenceinterface spellwindow mainmenu quickkeysmenu itemselection spellbuyingwindow loadingscreen levelupdialog waitdialog spellcreationdialog - enchantingdialog trainingwindow travelwindow imagebutton exposedwindow cursor spellicons + enchantingdialog trainingwindow travelwindow exposedwindow cursor spellicons merchantrepair repair soulgemdialog companionwindow bookpage journalviewmodel journalbooks - keywordsearch itemmodel containeritemmodel inventoryitemmodel sortfilteritemmodel itemview - tradeitemmodel companionitemmodel pickpocketitemmodel fontloader controllers savegamedialog - recharge mode videowidget backgroundimage + itemmodel containeritemmodel inventoryitemmodel sortfilteritemmodel itemview + tradeitemmodel companionitemmodel pickpocketitemmodel controllers savegamedialog + recharge mode videowidget backgroundimage itemwidget screenfader debugwindow ) add_openmw_dir (mwdialogue - dialoguemanagerimp journalimp journalentry quest topic filter selectwrapper + dialoguemanagerimp journalimp journalentry quest topic filter selectwrapper hypertextparser keywordsearch ) add_openmw_dir (mwscript @@ -48,7 +55,7 @@ add_openmw_dir (mwscript ) add_openmw_dir (mwsound - soundmanagerimp openal_output ffmpeg_decoder sound + soundmanagerimp openal_output ffmpeg_decoder sound sound_decoder sound_output loudness libavwrapper movieaudiofactory ) add_openmw_dir (mwworld @@ -57,7 +64,7 @@ add_openmw_dir (mwworld cells localscripts customdata weather inventorystore ptr actionopen actionread actionequip timestamp actionalchemy cellstore actionapply actioneat esmstore store recordcmp fallback actionrepair actionsoulgem livecellref actiondoor - contentloader esmloader omwloader actiontrap cellreflist projectilemanager cellref + contentloader esmloader actiontrap cellreflist projectilemanager cellref ) add_openmw_dir (mwclass @@ -69,7 +76,7 @@ add_openmw_dir (mwmechanics mechanicsmanagerimp stat character creaturestats magiceffects movement actors objects drawstate spells activespells npcstats aipackage aisequence aipursue alchemy aiwander aitravel aifollow aiavoiddoor aiescort aiactivate aicombat repair enchanting pathfinding pathgrid security spellsuccess spellcasting - disease pickpocket levelledlist combat steering obstacle + disease pickpocket levelledlist combat steering obstacle autocalcspell difficultyscaling aicombataction ) add_openmw_dir (mwstate @@ -82,37 +89,37 @@ add_openmw_dir (mwbase ) # Main executable -set(BOOST_COMPONENTS system filesystem program_options thread wave) +if (ANDROID) + set(BOOST_COMPONENTS system filesystem program_options thread wave atomic) +else () + set(BOOST_COMPONENTS system filesystem program_options thread wave) +endif () + if(WIN32) set(BOOST_COMPONENTS ${BOOST_COMPONENTS} locale) endif(WIN32) find_package(Boost REQUIRED COMPONENTS ${BOOST_COMPONENTS}) -IF(OGRE_STATIC) -ADD_DEFINITIONS(-DENABLE_PLUGIN_OctreeSceneManager -DENABLE_PLUGIN_ParticleFX -DENABLE_PLUGIN_GL) -set(OGRE_STATIC_PLUGINS ${OGRE_Plugin_OctreeSceneManager_LIBRARIES} ${OGRE_Plugin_ParticleFX_LIBRARIES} ${OGRE_RenderSystem_GL_LIBRARIES}) -IF(WIN32) -ADD_DEFINITIONS(-DENABLE_PLUGIN_Direct3D9) -list (APPEND OGRE_STATIC_PLUGINS ${OGRE_RenderSystem_Direct3D9_LIBRARIES}) -ENDIF(WIN32) -IF (Cg_FOUND) -ADD_DEFINITIONS(-DENABLE_PLUGIN_CgProgramManager) -list (APPEND OGRE_STATIC_PLUGINS ${OGRE_Plugin_CgProgramManager_LIBRARIES} ${Cg_LIBRARIES}) -ENDIF (Cg_FOUND) -ENDIF(OGRE_STATIC) - -add_executable(openmw - ${OPENMW_LIBS} ${OPENMW_LIBS_HEADER} - ${OPENMW_FILES} - ${GAME} ${GAME_HEADER} - ${APPLE_BUNDLE_RESOURCES} -) +if (NOT ANDROID) + add_executable(openmw + ${OPENMW_LIBS} ${OPENMW_LIBS_HEADER} + ${OPENMW_FILES} + ${GAME} ${GAME_HEADER} + ${APPLE_BUNDLE_RESOURCES} + ) +else () + add_library(openmw + SHARED + ${OPENMW_LIBS} ${OPENMW_LIBS_HEADER} + ${OPENMW_FILES} + ${GAME} ${GAME_HEADER} + ) +endif () # Sound stuff - here so CMake doesn't stupidly recompile EVERYTHING # when we change the backend. include_directories(${SOUND_INPUT_INCLUDES} ${BULLET_INCLUDE_DIRS}) -add_definitions(${SOUND_DEFINE}) target_link_libraries(openmw ${OGRE_LIBRARIES} @@ -125,11 +132,29 @@ target_link_libraries(openmw ${MYGUI_LIBRARIES} ${SDL2_LIBRARY} ${MYGUI_PLATFORM_LIBRARIES} + "ogre-ffmpeg-videoplayer" "oics" "sdl4ogre" components ) +if (ANDROID) + target_link_libraries(openmw + ${OGRE_STATIC_PLUGINS} + EGL + android + log + dl + MyGUI.OgrePlatform + MyGUIEngineStatic + Plugin_StrangeButtonStatic + cpufeatures + BulletCollision + BulletDynamics + LinearMath + ) +endif (ANDROID) + if (USE_SYSTEM_TINYXML) target_link_libraries(openmw ${TINYXML_LIBRARIES}) endif() @@ -155,10 +180,6 @@ if(APPLE) endif() endif(APPLE) -if(DPKG_PROGRAM) - INSTALL(TARGETS openmw RUNTIME DESTINATION games COMPONENT openmw) -endif(DPKG_PROGRAM) - if (BUILD_WITH_CODE_COVERAGE) add_definitions (--coverage) target_link_libraries(openmw gcov) diff --git a/apps/openmw/android_main.c b/apps/openmw/android_main.c new file mode 100644 index 000000000..76da91c4f --- /dev/null +++ b/apps/openmw/android_main.c @@ -0,0 +1,43 @@ + +#include "../../SDL_internal.h" + +#ifdef __ANDROID__ +#include "SDL_main.h" + + +/******************************************************************************* + Functions called by JNI +*******************************************************************************/ +#include + +/* Called before to initialize JNI bindings */ + + + +extern void SDL_Android_Init(JNIEnv* env, jclass cls); + + +int Java_org_libsdl_app_SDLActivity_nativeInit(JNIEnv* env, jclass cls, jobject obj) +{ + + SDL_Android_Init(env, cls); + + SDL_SetMainReady(); + + +/* Run the application code! */ + + int status; + char *argv[2]; + argv[0] = SDL_strdup("openmw"); + argv[1] = NULL; + status = main(1, argv); + + /* Do not issue an exit or the whole application will terminate instead of just the SDL thread */ + /* exit(status); */ + + return status; +} + +#endif /* __ANDROID__ */ + diff --git a/apps/openmw/crashcatcher.cpp b/apps/openmw/crashcatcher.cpp index 65a036919..b9d78540e 100644 --- a/apps/openmw/crashcatcher.cpp +++ b/apps/openmw/crashcatcher.cpp @@ -37,6 +37,10 @@ static const char pipe_err[] = "!!! Failed to create pipe\n"; static const char fork_err[] = "!!! Failed to fork debug process\n"; static const char exec_err[] = "!!! Failed to exec debug process\n"; +#ifndef PATH_MAX /* Not all platforms (GNU Hurd) have this. */ +# define PATH_MAX 256 +#endif + static char argv0[PATH_MAX]; static char altstack[SIGSTKSZ]; @@ -66,7 +70,7 @@ static const struct { int code; const char *name; } sigill_codes[] = { - #ifndef __FreeBSD__ + #if !defined(__FreeBSD__) && !defined(__FreeBSD_kernel__) { ILL_ILLOPC, "Illegal opcode" }, { ILL_ILLOPN, "Illegal operand" }, { ILL_ILLADR, "Illegal addressing mode" }, @@ -123,12 +127,11 @@ static int (*cc_user_info)(char*, char*); static void gdb_info(pid_t pid) { char respfile[64]; - char cmd_buf[128]; FILE *f; int fd; /* Create a temp file to put gdb commands into */ - strcpy(respfile, "gdb-respfile-XXXXXX"); + strcpy(respfile, "/tmp/gdb-respfile-XXXXXX"); if((fd=mkstemp(respfile)) >= 0 && (f=fdopen(fd, "w")) != NULL) { fprintf(f, "attach %d\n" @@ -152,6 +155,7 @@ static void gdb_info(pid_t pid) fclose(f); /* Run gdb and print process info. */ + char cmd_buf[128]; snprintf(cmd_buf, sizeof(cmd_buf), "gdb --quiet --batch --command=%s", respfile); printf("Executing: %s\n", cmd_buf); fflush(stdout); @@ -381,10 +385,7 @@ static void crash_handler(const char *logfile) if(logfile) { - char cwd[MAXPATHLEN]; - getcwd(cwd, MAXPATHLEN); - - std::string message = "OpenMW has encountered a fatal error.\nCrash log saved to '" + std::string(cwd) + "/" + std::string(logfile) + "'.\n Please report this to https://bugs.openmw.org !"; + std::string message = "OpenMW has encountered a fatal error.\nCrash log saved to '" + std::string(logfile) + "'.\n Please report this to https://bugs.openmw.org !"; SDL_ShowSimpleMessageBox(0, "Fatal Error", message.c_str(), NULL); } exit(0); diff --git a/apps/openmw/engine.cpp b/apps/openmw/engine.cpp index d33e5300e..2eebb8c28 100644 --- a/apps/openmw/engine.cpp +++ b/apps/openmw/engine.cpp @@ -15,7 +15,6 @@ #include #include #include -#include #include #include @@ -87,11 +86,20 @@ bool OMW::Engine::frameRenderingQueued (const Ogre::FrameEvent& evt) // update input MWBase::Environment::get().getInputManager()->update(frametime, false); + // When the window is minimized, pause everything. Currently this *has* to be here to work around a MyGUI bug. + // If we are not currently rendering, then RenderItems will not be reused resulting in a memory leak upon changing widget textures. + if (!mOgre->getWindow()->isActive() || !mOgre->getWindow()->isVisible()) + return true; + // sound if (mUseSound) MWBase::Environment::get().getSoundManager()->update(frametime); - bool paused = MWBase::Environment::get().getWindowManager()->isGuiMode(); + // GUI active? Most game processing will be paused, but scripts still run. + bool guiActive = MWBase::Environment::get().getWindowManager()->isGuiMode(); + + // Main menu opened? Then scripts are also paused. + bool paused = MWBase::Environment::get().getWindowManager()->containsMode(MWGui::GM_MainMenu); // update game state MWBase::Environment::get().getStateManager()->update (frametime); @@ -99,15 +107,18 @@ bool OMW::Engine::frameRenderingQueued (const Ogre::FrameEvent& evt) if (MWBase::Environment::get().getStateManager()->getState()== MWBase::StateManager::State_Running) { - // global scripts - MWBase::Environment::get().getScriptManager()->getGlobalScripts().run(); + if (!paused) + { + // local scripts + executeLocalScripts(); - // local scripts - executeLocalScripts(); + // global scripts + MWBase::Environment::get().getScriptManager()->getGlobalScripts().run(); - MWBase::Environment::get().getWorld()->markCellAsUnchanged(); + MWBase::Environment::get().getWorld()->markCellAsUnchanged(); + } - if (!paused) + if (!guiActive) MWBase::Environment::get().getWorld()->advanceTime( frametime*MWBase::Environment::get().getWorld()->getTimeScaleFactor()/3600); } @@ -118,14 +129,14 @@ bool OMW::Engine::frameRenderingQueued (const Ogre::FrameEvent& evt) MWBase::StateManager::State_NoGame) { MWBase::Environment::get().getMechanicsManager()->update(frametime, - paused); + guiActive); } if (MWBase::Environment::get().getStateManager()->getState()== MWBase::StateManager::State_Running) { MWWorld::Ptr player = mEnvironment.getWorld()->getPlayerPtr(); - if(!paused && player.getClass().getCreatureStats(player).isDead()) + if(!guiActive && player.getClass().getCreatureStats(player).isDead()) MWBase::Environment::get().getStateManager()->endGame(); } @@ -133,10 +144,11 @@ bool OMW::Engine::frameRenderingQueued (const Ogre::FrameEvent& evt) if (MWBase::Environment::get().getStateManager()->getState()!= MWBase::StateManager::State_NoGame) { - MWBase::Environment::get().getWorld()->update(frametime, paused); + MWBase::Environment::get().getWorld()->update(frametime, guiActive); } // update GUI + MWBase::Environment::get().getWindowManager()->onFrame(frametime); if (MWBase::Environment::get().getStateManager()->getState()!= MWBase::StateManager::State_NoGame) { @@ -145,7 +157,6 @@ bool OMW::Engine::frameRenderingQueued (const Ogre::FrameEvent& evt) MWBase::Environment::get().getWorld()->getTriangleBatchCount(tri, batch); MWBase::Environment::get().getWindowManager()->wmUpdateFps(window->getLastFPS(), tri, batch); - MWBase::Environment::get().getWindowManager()->onFrame(frametime); MWBase::Environment::get().getWindowManager()->update(); } } @@ -173,7 +184,9 @@ OMW::Engine::Engine(Files::ConfigurationManager& configurationManager) , mEncoder(NULL) , mActivationDistanceOverride(-1) , mGrab(true) - + , mScriptBlacklistUse (true) + , mExportFonts(false) + , mNewGame (false) { std::srand ( std::time(NULL) ); MWClass::registerClasses(); @@ -256,9 +269,10 @@ void OMW::Engine::setScriptsVerbosity(bool scriptsVerbosity) mVerboseScripts = scriptsVerbosity; } -void OMW::Engine::setSkipMenu (bool skipMenu) +void OMW::Engine::setSkipMenu (bool skipMenu, bool newGame) { mSkipMenu = skipMenu; + mNewGame = newGame; } std::string OMW::Engine::loadSettings (Settings::Manager & settings) @@ -307,8 +321,6 @@ void OMW::Engine::prepareEngine (Settings::Manager & settings) mEnvironment.setStateManager ( new MWState::StateManager (mCfgMgr.getUserDataPath() / "saves", mContentFiles.at (0))); - Nif::NIFFile::CacheLock cachelock; - std::string renderSystem = settings.getString("render system", "Video"); if (renderSystem == "") { @@ -329,8 +341,6 @@ void OMW::Engine::prepareEngine (Settings::Manager & settings) // This has to be added BEFORE MyGUI is initialized, as it needs // to find core.xml here. - //addResourcesDirectory(mResDir); - addResourcesDirectory(mCfgMgr.getCachePath ().string()); addResourcesDirectory(mResDir / "mygui"); @@ -357,14 +367,14 @@ void OMW::Engine::prepareEngine (Settings::Manager & settings) // Create input and UI first to set up a bootstrapping environment for // showing a loading screen and keeping the window responsive while doing so - std::string keybinderUser = (mCfgMgr.getUserConfigPath() / "input.xml").string(); + std::string keybinderUser = (mCfgMgr.getUserConfigPath() / "input_v2.xml").string(); bool keybinderUserExists = boost::filesystem::exists(keybinderUser); MWInput::InputManager* input = new MWInput::InputManager (*mOgre, *this, keybinderUser, keybinderUserExists, mGrab); mEnvironment.setInputManager (input); MWGui::WindowManager* window = new MWGui::WindowManager( mExtensions, mFpsLevel, mOgre, mCfgMgr.getLogPath().string() + std::string("/"), - mCfgMgr.getCachePath ().string(), mScriptConsoleMode, mTranslationDataStorage, mEncoding); + mCfgMgr.getCachePath ().string(), mScriptConsoleMode, mTranslationDataStorage, mEncoding, mExportFonts, mFallbackMap); mEnvironment.setWindowManager (window); // Create sound system @@ -399,7 +409,8 @@ void OMW::Engine::prepareEngine (Settings::Manager & settings) mScriptContext->setExtensions (&mExtensions); mEnvironment.setScriptManager (new MWScript::ScriptManager (MWBase::Environment::get().getWorld()->getStore(), - mVerboseScripts, *mScriptContext, mWarningsMode)); + mVerboseScripts, *mScriptContext, mWarningsMode, + mScriptBlacklistUse ? mScriptBlacklist : std::vector())); // Create game mechanics system MWMechanics::MechanicsManager* mechanics = new MWMechanics::MechanicsManager; @@ -463,11 +474,11 @@ void OMW::Engine::go() } else { - MWBase::Environment::get().getStateManager()->newGame (true); + MWBase::Environment::get().getStateManager()->newGame (!mNewGame); } // Start the main rendering loop - while (!mEnvironment.get().getStateManager()->hasQuitRequest()) + while (!MWBase::Environment::get().getStateManager()->hasQuitRequest()) Ogre::Root::getSingleton().renderOneFrame(); // Save user settings @@ -489,24 +500,7 @@ void OMW::Engine::activate() if (ptr.getClass().getName(ptr) == "") // objects without name presented to user can never be activated return; - MWScript::InterpreterContext interpreterContext (&ptr.getRefData().getLocals(), ptr); - - interpreterContext.activate (ptr); - - std::string script = ptr.getClass().getScript (ptr); - - MWBase::Environment::get().getWorld()->breakInvisibility(MWBase::Environment::get().getWorld()->getPlayerPtr()); - - if (!script.empty()) - { - MWBase::Environment::get().getWorld()->getLocalScripts().setIgnore (ptr); - MWBase::Environment::get().getScriptManager()->run (script, interpreterContext); - } - - if (!interpreterContext.hasActivationBeenHandled()) - { - interpreterContext.executeActivation(ptr); - } + MWBase::Environment::get().getWorld()->activate(ptr, MWBase::Environment::get().getWorld()->getPlayerPtr()); } void OMW::Engine::screenshot() @@ -515,7 +509,7 @@ void OMW::Engine::screenshot() int shotCount = 0; const std::string& screenshotPath = mCfgMgr.getUserDataPath().string(); - + std::string format = Settings::Manager::getString("screenshot format", "General"); // Find the first unused filename with a do-while std::ostringstream stream; do @@ -524,11 +518,11 @@ void OMW::Engine::screenshot() stream.str(""); stream.clear(); - stream << screenshotPath << "screenshot" << std::setw(3) << std::setfill('0') << shotCount++ << ".png"; + stream << screenshotPath << "screenshot" << std::setw(3) << std::setfill('0') << shotCount++ << "." << format; } while (boost::filesystem::exists(stream.str())); - mOgre->screenshot(stream.str()); + mOgre->screenshot(stream.str(), format); } void OMW::Engine::setCompileAll (bool all) @@ -575,3 +569,18 @@ void OMW::Engine::setWarningsMode (int mode) { mWarningsMode = mode; } + +void OMW::Engine::setScriptBlacklist (const std::vector& list) +{ + mScriptBlacklist = list; +} + +void OMW::Engine::setScriptBlacklistUse (bool use) +{ + mScriptBlacklistUse = use; +} + +void OMW::Engine::enableFontExport(bool exportFonts) +{ + mExportFonts = exportFonts; +} diff --git a/apps/openmw/engine.hpp b/apps/openmw/engine.hpp index e0f51d0dc..0ee5a09c5 100644 --- a/apps/openmw/engine.hpp +++ b/apps/openmw/engine.hpp @@ -7,6 +7,8 @@ #include #include #include +#include + #include "mwbase/environment.hpp" @@ -83,12 +85,19 @@ namespace OMW // Grab mouse? bool mGrab; + bool mExportFonts; + Compiler::Extensions mExtensions; Compiler::Context *mScriptContext; Files::Collections mFileCollections; bool mFSStrict; Translation::Storage mTranslationDataStorage; + std::vector mScriptBlacklist; + bool mScriptBlacklistUse; + bool mNewGame; + + Nif::Cache mNifCache; // not implemented Engine (const Engine&); @@ -149,7 +158,11 @@ namespace OMW /// Disable or enable all sounds void setSoundUsage(bool soundUsage); - void setSkipMenu (bool skipMenu); + /// Skip main menu and go directly into the game + /// + /// \param newGame Start a new game instead off dumping the player into the game + /// (ignored if !skipMenu). + void setSkipMenu (bool skipMenu, bool newGame); void setGrabMouse(bool grab) { mGrab = grab; } @@ -181,6 +194,12 @@ namespace OMW void setWarningsMode (int mode); + void setScriptBlacklist (const std::vector& list); + + void setScriptBlacklistUse (bool use); + + void enableFontExport(bool exportFonts); + private: Files::ConfigurationManager& mCfgMgr; }; diff --git a/apps/openmw/main.cpp b/apps/openmw/main.cpp index 3098d953e..744780b25 100644 --- a/apps/openmw/main.cpp +++ b/apps/openmw/main.cpp @@ -4,19 +4,20 @@ #include #include -#include +#include +#include #include "engine.hpp" -#if defined(_WIN32) && !defined(_CONSOLE) #include #include +#include +#if defined(_WIN32) // For OutputDebugString #define WIN32_LEAN_AND_MEAN #include // makes __argc and __argv available on windows #include - #endif @@ -61,7 +62,6 @@ void validate(boost::any &v, std::vector const &tokens, FallbackMap FallbackMap *map = boost::any_cast(&v); - std::map::iterator mapIt; for(std::vector::const_iterator it=tokens.begin(); it != tokens.end(); ++it) { int sep = it->find(","); @@ -75,7 +75,7 @@ void validate(boost::any &v, std::vector const &tokens, FallbackMap std::string key(it->substr(0,sep)); std::string value(it->substr(sep+1)); - if((mapIt = map->mMap.find(key)) == map->mMap.end()) + if(map->mMap.find(key) == map->mMap.end()) { map->mMap.insert(std::make_pair (key,value)); } @@ -104,7 +104,7 @@ bool parseOptions (int argc, char** argv, OMW::Engine& engine, Files::Configurat ("help", "print help message") ("version", "print version information and quit") ("data", bpo::value()->default_value(Files::PathContainer(), "data") - ->multitoken(), "set data directories (later directories have higher priority)") + ->multitoken()->composing(), "set data directories (later directories have higher priority)") ("data-local", bpo::value()->default_value(""), "set local data directory (highest priority)") @@ -143,9 +143,18 @@ bool parseOptions (int argc, char** argv, OMW::Engine& engine, Files::Configurat "\t1 - show warning but consider script as correctly compiled anyway\n" "\t2 - treat warnings as errors") + ("script-blacklist", bpo::value()->default_value(StringsVector(), "") + ->multitoken(), "ignore the specified script (if the use of the blacklist is enabled)") + + ("script-blacklist-use", bpo::value()->implicit_value(true) + ->default_value(true), "enable script blacklisting") + ("skip-menu", bpo::value()->implicit_value(true) ->default_value(false), "skip main menu on game startup") + ("new-game", bpo::value()->implicit_value(true) + ->default_value(false), "run new game sequence (ignored if skip-menu=0)") + ("fs-strict", bpo::value()->implicit_value(true) ->default_value(false), "strict file system handling (no case folding)") @@ -161,6 +170,9 @@ bool parseOptions (int argc, char** argv, OMW::Engine& engine, Files::Configurat ("no-grab", "Don't grab mouse cursor") + ("export-fonts", bpo::value()->implicit_value(true) + ->default_value(false), "Export Morrowind .fnt fonts to PNG image and XML file in current directory") + ("activate-dist", bpo::value ()->default_value (-1), "activation distance override"); bpo::parsed_options valid_opts = bpo::command_line_parser(argc, argv) @@ -183,6 +195,14 @@ bool parseOptions (int argc, char** argv, OMW::Engine& engine, Files::Configurat if (variables.count ("version")) { std::cout << "OpenMW version " << OPENMW_VERSION << std::endl; + + std::string rev = OPENMW_VERSION_COMMITHASH; + std::string tag = OPENMW_VERSION_TAGHASH; + if (!rev.empty() && !tag.empty()) + { + rev = rev.substr(0, 10); + std::cout << "Revision " << rev << std::endl; + } run = false; } @@ -238,73 +258,30 @@ bool parseOptions (int argc, char** argv, OMW::Engine& engine, Files::Configurat // startup-settings engine.setCell(variables["start"].as()); - engine.setSkipMenu (variables["skip-menu"].as()); + engine.setSkipMenu (variables["skip-menu"].as(), variables["new-game"].as()); + if (!variables["skip-menu"].as() && variables["new-game"].as()) + std::cerr << "new-game used without skip-menu -> ignoring it" << std::endl; - // other settings - engine.setSoundUsage(!variables["no-sound"].as()); - engine.setScriptsVerbosity(variables["script-verbose"].as()); + // scripts engine.setCompileAll(variables["script-all"].as()); - engine.setFallbackValues(variables["fallback"].as().mMap); + engine.setScriptsVerbosity(variables["script-verbose"].as()); engine.setScriptConsoleMode (variables["script-console"].as()); engine.setStartupScript (variables["script-run"].as()); - engine.setActivationDistanceOverride (variables["activate-dist"].as()); engine.setWarningsMode (variables["script-warn"].as()); + engine.setScriptBlacklist (variables["script-blacklist"].as()); + engine.setScriptBlacklistUse (variables["script-blacklist-use"].as()); + + // other settings + engine.setSoundUsage(!variables["no-sound"].as()); + engine.setFallbackValues(variables["fallback"].as().mMap); + engine.setActivationDistanceOverride (variables["activate-dist"].as()); + engine.enableFontExport(variables["export-fonts"].as()); return true; } -int main(int argc, char**argv) -{ -#if OGRE_PLATFORM == OGRE_PLATFORM_LINUX || OGRE_PLATFORM == OGRE_PLATFORM_APPLE - // 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, "crash.log", NULL); - std::cout << "Installing crash catcher" << std::endl; - } - else - std::cout << "Running in a debugger, not installing crash catcher" << std::endl; -#endif - -#if OGRE_PLATFORM == OGRE_PLATFORM_APPLE - // set current dir to bundle path - boost::filesystem::path bundlePath = boost::filesystem::path(Ogre::macBundlePath()).parent_path(); - boost::filesystem::current_path(bundlePath); -#endif - - try - { - Files::ConfigurationManager cfgMgr; - OMW::Engine engine(cfgMgr); - - if (parseOptions(argc, argv, engine, cfgMgr)) - { - engine.go(); - } - } - catch (std::exception &e) - { -#if OGRE_PLATFORM == OGRE_PLATFORM_LINUX || OGRE_PLATFORM == OGRE_PLATFORM_APPLE - if (isatty(fileno(stdin)) || !SDL_WasInit(SDL_INIT_VIDEO)) - std::cerr << "\nERROR: " << e.what() << std::endl; - else -#endif - SDL_ShowSimpleMessageBox(0, "OpenMW: Fatal error", e.what(), NULL); - - return 1; - } +#if defined(_WIN32) && defined(_DEBUG) - return 0; -} - -// Platform specific for Windows when there is no console built into the executable. -// Windows will call the WinMain function instead of main in this case, the normal -// main function is then called with the __argc and __argv parameters. -// In addition if it is a debug build it will redirect cout to the debug console in Visual Studio -#if defined(_WIN32) && !defined(_CONSOLE) - -#if defined(_DEBUG) class DebugOutput : public boost::iostreams::sink { public: @@ -318,11 +295,11 @@ public: } }; #else -class Logger : public boost::iostreams::sink +class Tee : public boost::iostreams::sink { public: - Logger(std::ofstream &stream) - : out(stream) + Tee(std::ostream &stream, std::ostream &stream2) + : out(stream), out2(stream2) { } @@ -330,38 +307,109 @@ public: { out.write (str, size); out.flush(); + out2.write (str, size); + out2.flush(); return size; } private: - std::ofstream &out; + std::ostream &out; + std::ostream &out2; }; #endif -int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd) +int main(int argc, char**argv) { - std::streambuf* old_rdbuf = std::cout.rdbuf (); + // Some objects used to redirect cout and cerr + // Scope must be here, so this still works inside the catch block for logging exceptions + std::streambuf* cout_rdbuf = std::cout.rdbuf (); + std::streambuf* cerr_rdbuf = std::cerr.rdbuf (); + +#if !(defined(_WIN32) && defined(_DEBUG)) + boost::iostreams::stream_buffer coutsb; + boost::iostreams::stream_buffer cerrsb; +#endif + + std::ostream oldcout(cout_rdbuf); + std::ostream oldcerr(cerr_rdbuf); + + boost::filesystem::ofstream logfile; + + std::auto_ptr engine; int ret = 0; -#if defined(_DEBUG) - // Redirect cout to VS debug output when running in debug mode + try { + Files::ConfigurationManager cfgMgr; + +#if defined(_WIN32) && defined(_DEBUG) + // Redirect cout and cerr to VS debug output when running in debug mode boost::iostreams::stream_buffer sb; sb.open(DebugOutput()); + std::cout.rdbuf (&sb); + std::cerr.rdbuf (&sb); #else - // Redirect cout to openmw.log - std::ofstream logfile ("openmw.log"); - { - boost::iostreams::stream_buffer sb; - sb.open (Logger (logfile)); + // Redirect cout and cerr to openmw.log + logfile.open (boost::filesystem::path(cfgMgr.getLogPath() / "/openmw.log")); + + coutsb.open (Tee(logfile, oldcout)); + cerrsb.open (Tee(logfile, oldcerr)); + + std::cout.rdbuf (&coutsb); + std::cerr.rdbuf (&cerrsb); #endif - std::cout.rdbuf (&sb); - ret = main (__argc, __argv); - std::cout.rdbuf(old_rdbuf); +#if OGRE_PLATFORM == OGRE_PLATFORM_LINUX || OGRE_PLATFORM == OGRE_PLATFORM_APPLE + // 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 + +#if OGRE_PLATFORM == OGRE_PLATFORM_APPLE + // set current dir to bundle path + boost::filesystem::path bundlePath = boost::filesystem::path(Ogre::macBundlePath()).parent_path(); + boost::filesystem::current_path(bundlePath); +#endif + + engine.reset(new OMW::Engine(cfgMgr)); + + if (parseOptions(argc, argv, *engine, cfgMgr)) + { + engine->go(); + } + } + catch (std::exception &e) + { +#if OGRE_PLATFORM == OGRE_PLATFORM_LINUX || OGRE_PLATFORM == OGRE_PLATFORM_APPLE + if (isatty(fileno(stdin))) + std::cerr << "\nERROR: " << e.what() << std::endl; + else +#endif + SDL_ShowSimpleMessageBox(0, "OpenMW: Fatal error", e.what(), NULL); + + ret = 1; } + + // Restore cout and cerr + std::cout.rdbuf(cout_rdbuf); + std::cerr.rdbuf(cerr_rdbuf); + return ret; } +// Platform specific for Windows when there is no console built into the executable. +// Windows will call the WinMain function instead of main in this case, the normal +// main function is then called with the __argc and __argv parameters. +#if defined(_WIN32) && !defined(_CONSOLE) +int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd) +{ + return main(__argc, __argv); +} #endif diff --git a/apps/openmw/mwbase/dialoguemanager.hpp b/apps/openmw/mwbase/dialoguemanager.hpp index cab6809aa..d0e64b23c 100644 --- a/apps/openmw/mwbase/dialoguemanager.hpp +++ b/apps/openmw/mwbase/dialoguemanager.hpp @@ -61,6 +61,8 @@ namespace MWBase virtual void persuade (int type) = 0; virtual int getTemporaryDispositionChange () const = 0; + + /// @note This change is temporary and gets discarded when dialogue ends. virtual void applyDispositionChange (int delta) = 0; virtual int countSavedGameRecords() const = 0; @@ -74,6 +76,9 @@ namespace MWBase /// @return faction1's opinion of faction2 virtual int getFactionReaction (const std::string& faction1, const std::string& faction2) const = 0; + + /// Removes the last added topic response for the given actor from the journal + virtual void clearInfoActor (const MWWorld::Ptr& actor) const = 0; }; } diff --git a/apps/openmw/mwbase/journal.hpp b/apps/openmw/mwbase/journal.hpp index a49ebb9bc..938cec74b 100644 --- a/apps/openmw/mwbase/journal.hpp +++ b/apps/openmw/mwbase/journal.hpp @@ -59,7 +59,12 @@ namespace MWBase virtual int getJournalIndex (const std::string& id) const = 0; ///< Get the journal index. - virtual void addTopic (const std::string& topicId, const std::string& infoId, const std::string& actorName) = 0; + virtual void addTopic (const std::string& topicId, const std::string& infoId, const MWWorld::Ptr& actor) = 0; + /// \note topicId must be lowercase + + virtual void removeLastAddedTopicResponse (const std::string& topicId, const std::string& actorName) = 0; + ///< Removes the last topic response added for the given topicId and actor name. + /// \note topicId must be lowercase virtual TEntryIter begin() const = 0; ///< Iterator pointing to the begin of the main journal. diff --git a/apps/openmw/mwbase/mechanicsmanager.hpp b/apps/openmw/mwbase/mechanicsmanager.hpp index f31241bdb..ce213b316 100644 --- a/apps/openmw/mwbase/mechanicsmanager.hpp +++ b/apps/openmw/mwbase/mechanicsmanager.hpp @@ -4,6 +4,7 @@ #include #include #include +#include namespace Ogre { @@ -13,6 +14,9 @@ namespace Ogre namespace ESM { struct Class; + + class ESMReader; + class ESMWriter; } namespace MWWorld @@ -21,6 +25,11 @@ namespace MWWorld class CellStore; } +namespace Loading +{ + class Listener; +} + namespace MWBase { /// \brief Interface for game mechanics manager (implemented in MWMechanics) @@ -111,6 +120,7 @@ namespace MWBase /** * @brief Commit a crime. If any actors witness the crime and report it, * reportCrime will be called automatically. + * @note victim may be empty * @param arg Depends on \a type, e.g. for Theft, the value of the item that was stolen. * @return was the crime reported? */ @@ -118,6 +128,8 @@ namespace MWBase OffenseType type, int arg=0) = 0; virtual void reportCrime (const MWWorld::Ptr& ptr, const MWWorld::Ptr& victim, OffenseType type, int arg=0) = 0; + /// @return false if the attack was considered a "friendly hit" and forgiven + virtual bool actorAttacked (const MWWorld::Ptr& victim, const MWWorld::Ptr& attacker) = 0; /// Utility to check if taking this item is illegal and calling commitCrime if so virtual void itemTaken (const MWWorld::Ptr& ptr, const MWWorld::Ptr& item, int count) = 0; /// Utility to check if opening (i.e. unlocking) this object is illegal and calling commitCrime if so @@ -126,6 +138,9 @@ namespace MWBase /// @return was it illegal, and someone saw you doing it? virtual bool sleepInBed (const MWWorld::Ptr& ptr, const MWWorld::Ptr& bed) = 0; + /// @return is \a ptr allowed to take/use \a item or is it a crime? + virtual bool isAllowedToUse (const MWWorld::Ptr& ptr, const MWWorld::Ptr& item, MWWorld::Ptr& victim) = 0; + enum PersuasionType { PT_Admire, @@ -174,6 +189,21 @@ namespace MWBase virtual std::list getActorsFighting(const MWWorld::Ptr& actor) = 0; virtual void playerLoaded() = 0; + + virtual int countSavedGameRecords() const = 0; + + virtual void write (ESM::ESMWriter& writer, Loading::Listener& listener) const = 0; + + virtual void readRecord (ESM::ESMReader& reader, int32_t type) = 0; + + virtual void clear() = 0; + + /// @param bias Can be used to add an additional aggression bias towards the target, + /// making it more likely for the function to return true. + virtual bool isAggressive (const MWWorld::Ptr& ptr, const MWWorld::Ptr& target, int bias=0, bool ignoreDistance=false) = 0; + + /// Resurrects the player if necessary + virtual void keepPlayerAlive() = 0; }; } diff --git a/apps/openmw/mwbase/scriptmanager.hpp b/apps/openmw/mwbase/scriptmanager.hpp index ae146e064..7bdeba132 100644 --- a/apps/openmw/mwbase/scriptmanager.hpp +++ b/apps/openmw/mwbase/scriptmanager.hpp @@ -46,17 +46,11 @@ namespace MWBase ///< Compile all scripts /// \return count, success - virtual Compiler::Locals& getLocals (const std::string& name) = 0; + virtual const Compiler::Locals& getLocals (const std::string& name) = 0; ///< Return locals for script \a name. virtual MWScript::GlobalScripts& getGlobalScripts() = 0; - - virtual int getLocalIndex (const std::string& scriptId, const std::string& variable, - char type) = 0; - ///< Return index of the variable of the given name and type in the given script. Will - /// throw an exception, if there is no such script or variable or the type does not match. - - }; + }; } #endif diff --git a/apps/openmw/mwbase/soundmanager.hpp b/apps/openmw/mwbase/soundmanager.hpp index 15739730b..a02a463dd 100644 --- a/apps/openmw/mwbase/soundmanager.hpp +++ b/apps/openmw/mwbase/soundmanager.hpp @@ -101,6 +101,11 @@ namespace MWBase virtual void stopSay(const MWWorld::Ptr &reference=MWWorld::Ptr()) = 0; ///< Stop an actor speaking + virtual float getSaySoundLoudness(const MWWorld::Ptr& reference) const = 0; + ///< Check the currently playing say sound for this actor + /// and get an average loudness value (scale [0,1]) at the current time position. + /// If the actor is not saying anything, returns 0. + virtual SoundPtr playTrack(const MWSound::DecoderPtr& decoder, PlayType type) = 0; ///< Play a 2D audio track, using a custom decoder diff --git a/apps/openmw/mwbase/statemanager.hpp b/apps/openmw/mwbase/statemanager.hpp index 121a73a48..006be921b 100644 --- a/apps/openmw/mwbase/statemanager.hpp +++ b/apps/openmw/mwbase/statemanager.hpp @@ -1,7 +1,7 @@ #ifndef GAME_MWSTATE_STATEMANAGER_H #define GAME_MWSTATE_STATEMANAGER_H -#include +#include #include namespace MWState @@ -24,7 +24,7 @@ namespace MWBase State_Running }; - typedef std::vector::const_iterator CharacterIterator; + typedef std::list::const_iterator CharacterIterator; private: diff --git a/apps/openmw/mwbase/windowmanager.hpp b/apps/openmw/mwbase/windowmanager.hpp index 44ebed3e9..bfc4f3b33 100644 --- a/apps/openmw/mwbase/windowmanager.hpp +++ b/apps/openmw/mwbase/windowmanager.hpp @@ -57,6 +57,7 @@ namespace MWGui class InventoryWindow; class ContainerWindow; class DialogueWindow; + class WindowModal; enum ShowInDialogueMode { ShowInDialogueMode_IfPossible, @@ -144,8 +145,6 @@ namespace MWBase virtual MWGui::SpellWindow* getSpellWindow() = 0; virtual MWGui::Console* getConsole() = 0; - virtual MyGUI::Gui* getGui() const = 0; - virtual void wmUpdateFps(float fps, unsigned int triangleCount, unsigned int batchCount) = 0; /// Set value for the given ID. @@ -178,7 +177,7 @@ namespace MWBase virtual void changeCell(MWWorld::CellStore* cell) = 0; ///< change the active cell - virtual void setPlayerPos(const float x, const float y) = 0; + virtual void setPlayerPos(int cellX, int cellY, const float x, const float y) = 0; ///< set player position in map space virtual void setPlayerDir(const float x, const float y) = 0; @@ -226,7 +225,7 @@ namespace MWBase virtual void showCrosshair(bool show) = 0; virtual bool getSubtitlesEnabled() = 0; - virtual void toggleHud() = 0; + virtual bool toggleGui() = 0; virtual void disallowMouse() = 0; virtual void allowMouse() = 0; @@ -234,14 +233,19 @@ namespace MWBase virtual void addVisitedLocation(const std::string& name, int x, int y) = 0; + /// Hides dialog and schedules dialog to be deleted. virtual void removeDialog(OEngine::GUI::Layout* dialog) = 0; - ///< Hides dialog and schedules dialog to be deleted. + + ///Gracefully attempts to exit the topmost GUI mode + /** No guarentee of actually closing the window **/ + virtual void exitCurrentGuiMode() = 0; virtual void messageBox (const std::string& message, const std::vector& buttons = std::vector(), enum MWGui::ShowInDialogueMode showInDialogueMode = MWGui::ShowInDialogueMode_IfPossible) = 0; virtual void staticMessageBox(const std::string& message) = 0; virtual void removeStaticMessageBox() = 0; + + /// returns the index of the pressed button or -1 if no button was pressed (->MessageBoxmanager->InteractiveMessageBox) virtual int readPressedButton() = 0; - ///< returns the index of the pressed button or -1 if no button was pressed (->MessageBoxmanager->InteractiveMessageBox) virtual void onFrame (float frameDuration) = 0; @@ -309,6 +313,35 @@ namespace MWBase /// Does the current stack of GUI-windows permit saving? virtual bool isSavingAllowed() const = 0; + + /// Returns the current Modal + /** Used to send exit command to active Modal when Esc is pressed **/ + virtual MWGui::WindowModal* getCurrentModal() const = 0; + + /// Sets the current Modal + /** Used to send exit command to active Modal when Esc is pressed **/ + virtual void addCurrentModal(MWGui::WindowModal* input) = 0; + + /// Removes the top Modal + /** Used when one Modal adds another Modal + \param input Pointer to the current modal, to ensure proper modal is removed **/ + virtual void removeCurrentModal(MWGui::WindowModal* input) = 0; + + virtual void pinWindow (MWGui::GuiWindow window) = 0; + + /// Fade the screen in, over \a time seconds + virtual void fadeScreenIn(const float time) = 0; + /// Fade the screen out to black, over \a time seconds + virtual void fadeScreenOut(const float time) = 0; + /// Fade the screen to a specified percentage of black, over \a time seconds + virtual void fadeScreenTo(const int percent, const float time) = 0; + /// Darken the screen to a specified percentage + virtual void setBlindness(const int percent) = 0; + + virtual void activateHitOverlay(bool interrupt=true) = 0; + virtual void setWerewolfOverlay(bool set) = 0; + + virtual void toggleDebugWindow() = 0; }; } diff --git a/apps/openmw/mwbase/world.hpp b/apps/openmw/mwbase/world.hpp index 81bec6fe8..c1a889913 100644 --- a/apps/openmw/mwbase/world.hpp +++ b/apps/openmw/mwbase/world.hpp @@ -17,11 +17,6 @@ namespace Ogre namespace OEngine { - namespace Render - { - class Fader; - } - namespace Physic { class PhysicEngine; @@ -113,9 +108,6 @@ namespace MWBase virtual void readRecord (ESM::ESMReader& reader, int32_t type, const std::map& contentFileMap) = 0; - virtual OEngine::Render::Fader* getFader() = 0; - ///< \todo remove this function. Rendering details should not be exposed. - virtual MWWorld::CellStore *getExterior (int x, int y) = 0; virtual MWWorld::CellStore *getInterior (const std::string& name) = 0; @@ -127,6 +119,7 @@ namespace MWBase virtual void setWaterHeight(const float height) = 0; virtual bool toggleWater() = 0; + virtual bool toggleWorld() = 0; virtual void adjustSky() = 0; @@ -144,20 +137,23 @@ namespace MWBase virtual MWWorld::LocalScripts& getLocalScripts() = 0; virtual bool hasCellChanged() const = 0; - ///< Has the player moved to a different cell, since the last frame? + ///< Has the set of active cells changed, since the last frame? virtual bool isCellExterior() const = 0; virtual bool isCellQuasiExterior() const = 0; virtual Ogre::Vector2 getNorthVector (MWWorld::CellStore* cell) = 0; - ///< get north vector (OGRE coordinates) for given interior cell + ///< get north vector for given interior cell virtual void getDoorMarkers (MWWorld::CellStore* cell, std::vector& out) = 0; ///< get a list of teleport door markers for a given cell, to be displayed on the local map - virtual void getInteriorMapPosition (Ogre::Vector2 position, float& nX, float& nY, int &x, int& y) = 0; - ///< see MWRender::LocalMap::getInteriorMapPosition + virtual void worldToInteriorMapPosition (Ogre::Vector2 position, float& nX, float& nY, int &x, int& y) = 0; + ///< see MWRender::LocalMap::worldToInteriorMapPosition + + virtual Ogre::Vector2 interiorMapToWorldPosition (float nX, float nY, int x, int y) = 0; + ///< see MWRender::LocalMap::interiorMapToWorldPosition virtual bool isPositionExplored (float nX, float nY, int x, int y, bool interior) = 0; ///< see MWRender::LocalMap::isPositionExplored @@ -203,6 +199,10 @@ namespace MWBase virtual MWWorld::Ptr searchPtrViaActorId (int actorId) = 0; ///< Search is limited to the active cells. + virtual MWWorld::Ptr findContainer (const MWWorld::Ptr& ptr) = 0; + ///< Return a pointer to a liveCellRef which contains \a ptr. + /// \note Search is limited to the active cells. + /// \todo enable reference in the OGRE scene virtual void enable (const MWWorld::Ptr& ptr) = 0; @@ -271,8 +271,12 @@ namespace MWBase /// use the "Head" node, or alternatively the "Bip01 Head" node as a basis. virtual std::pair getHitContact(const MWWorld::Ptr &ptr, float distance) = 0; - virtual void adjustPosition (const MWWorld::Ptr& ptr) = 0; + virtual void adjustPosition (const MWWorld::Ptr& ptr, bool force) = 0; ///< Adjust position after load to be on ground. Must be called after model load. + /// @param force do this even if the ptr is flying + + virtual void fixPosition (const MWWorld::Ptr& actor) = 0; + ///< Attempt to fix position so that the Ptr is no longer inside collision geometry. virtual void deleteObject (const MWWorld::Ptr& ptr) = 0; @@ -395,11 +399,22 @@ namespace MWBase /// open or close a non-teleport door (depending on current state) virtual void activateDoor(const MWWorld::Ptr& door) = 0; - /// open or close a non-teleport door as specified - virtual void activateDoor(const MWWorld::Ptr& door, bool open) = 0; + /// update movement state of a non-teleport door as specified + /// @param state see MWClass::setDoorState + /// @note throws an exception when invoked on a teleport door + virtual void activateDoor(const MWWorld::Ptr& door, int state) = 0; virtual bool getPlayerStandingOn (const MWWorld::Ptr& object) = 0; ///< @return true if the player is standing on \a object virtual bool getActorStandingOn (const MWWorld::Ptr& object) = 0; ///< @return true if any actor is standing on \a object + virtual bool getPlayerCollidingWith(const MWWorld::Ptr& object) = 0; ///< @return true if the player is colliding with \a object + virtual bool getActorCollidingWith (const MWWorld::Ptr& object) = 0; ///< @return true if any actor is colliding with \a object + virtual void hurtStandingActors (const MWWorld::Ptr& object, float dmgPerSecond) = 0; + ///< Apply a health difference to any actors standing on \a object. + /// To hurt actors, healthPerSecond should be a positive value. For a negative value, actors will be healed. + virtual void hurtCollidingActors (const MWWorld::Ptr& object, float dmgPerSecond) = 0; + ///< Apply a health difference to any actors colliding with \a object. + /// To hurt actors, healthPerSecond should be a positive value. For a negative value, actors will be healed. + virtual float getWindSpeed() = 0; virtual void getContainersOwnedBy (const MWWorld::Ptr& npc, std::vector& out) = 0; @@ -407,7 +422,7 @@ namespace MWBase virtual void getItemsOwnedBy (const MWWorld::Ptr& npc, std::vector& out) = 0; ///< get all items in active cells owned by this Npc - virtual bool getLOS(const MWWorld::Ptr& npc,const MWWorld::Ptr& targetNpc) = 0; + virtual bool getLOS(const MWWorld::Ptr& actor,const MWWorld::Ptr& targetActor) = 0; ///< get Line of Sight (morrowind stupid implementation) virtual float getDistToNearestRayHit(const Ogre::Vector3& from, const Ogre::Vector3& dir, float maxDist) = 0; @@ -470,7 +485,7 @@ namespace MWBase virtual void launchMagicBolt (const std::string& model, const std::string& sound, const std::string& spellId, float speed, bool stack, const ESM::EffectList& effects, - const MWWorld::Ptr& actor, const std::string& sourceName) = 0; + const MWWorld::Ptr& caster, const std::string& sourceName, const Ogre::Vector3& fallbackDirection) = 0; virtual void launchProjectile (MWWorld::Ptr actor, MWWorld::Ptr projectile, const Ogre::Vector3& worldPos, const Ogre::Quaternion& orient, MWWorld::Ptr bow, float speed) = 0; @@ -515,8 +530,23 @@ namespace MWBase /// Spawn a blood effect for \a ptr at \a worldPosition virtual void spawnBloodEffect (const MWWorld::Ptr& ptr, const Ogre::Vector3& worldPosition) = 0; + virtual void spawnEffect (const std::string& model, const std::string& textureOverride, const Ogre::Vector3& worldPos) = 0; + virtual void explodeSpell (const Ogre::Vector3& origin, const ESM::EffectList& effects, const MWWorld::Ptr& caster, const std::string& id, const std::string& sourceName) = 0; + + virtual void activate (const MWWorld::Ptr& object, const MWWorld::Ptr& actor) = 0; + + /// @see MWWorld::WeatherManager::isInStorm + virtual bool isInStorm() const = 0; + + /// @see MWWorld::WeatherManager::getStormDirection + virtual Ogre::Vector3 getStormDirection() const = 0; + + /// Resets all actors in the current active cells to their original location within that cell. + virtual void resetActors() = 0; + + virtual bool isWalkingOnWater (const MWWorld::Ptr& actor) = 0; }; } diff --git a/apps/openmw/mwclass/activator.cpp b/apps/openmw/mwclass/activator.cpp index 043aadd35..bf02b4d05 100644 --- a/apps/openmw/mwclass/activator.cpp +++ b/apps/openmw/mwclass/activator.cpp @@ -25,6 +25,11 @@ namespace MWClass { + std::string Activator::getId (const MWWorld::Ptr& ptr) const + { + return ptr.get()->mBase->mId; + } + void Activator::insertObjectRendering (const MWWorld::Ptr& ptr, MWRender::RenderingInterface& renderingInterface) const { const std::string model = getModel(ptr); @@ -97,8 +102,7 @@ namespace MWClass std::string text; if (MWBase::Environment::get().getWindowManager()->getFullHelp()) { - text += MWGui::ToolTips::getMiscString(ptr.getCellRef().getOwner(), "Owner"); - text += MWGui::ToolTips::getMiscString(ptr.getCellRef().getFaction(), "Faction"); + text += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); text += MWGui::ToolTips::getMiscString(ref->mBase->mScript, "Script"); } info.text = text; diff --git a/apps/openmw/mwclass/activator.hpp b/apps/openmw/mwclass/activator.hpp index 1e772ef4f..3e4bc3de4 100644 --- a/apps/openmw/mwclass/activator.hpp +++ b/apps/openmw/mwclass/activator.hpp @@ -13,6 +13,9 @@ namespace MWClass public: + /// Return ID of \a ptr + virtual std::string getId (const MWWorld::Ptr& ptr) const; + virtual void insertObjectRendering (const MWWorld::Ptr& ptr, MWRender::RenderingInterface& renderingInterface) const; ///< Add reference into a cell for rendering diff --git a/apps/openmw/mwclass/apparatus.cpp b/apps/openmw/mwclass/apparatus.cpp index 947a9cb94..c466cbc33 100644 --- a/apps/openmw/mwclass/apparatus.cpp +++ b/apps/openmw/mwclass/apparatus.cpp @@ -21,6 +21,11 @@ namespace MWClass { + std::string Apparatus::getId (const MWWorld::Ptr& ptr) const + { + return ptr.get()->mBase->mId; + } + void Apparatus::insertObjectRendering (const MWWorld::Ptr& ptr, MWRender::RenderingInterface& renderingInterface) const { const std::string model = getModel(ptr); @@ -124,11 +129,10 @@ namespace MWClass std::string text; text += "\n#{sQuality}: " + MWGui::ToolTips::toString(ref->mBase->mData.mQuality); text += "\n#{sWeight}: " + MWGui::ToolTips::toString(ref->mBase->mData.mWeight); - text += MWGui::ToolTips::getValueString(getValue(ptr), "#{sValue}"); + text += MWGui::ToolTips::getValueString(ref->mBase->mData.mValue, "#{sValue}"); if (MWBase::Environment::get().getWindowManager()->getFullHelp()) { - text += MWGui::ToolTips::getMiscString(ptr.getCellRef().getOwner(), "Owner"); - text += MWGui::ToolTips::getMiscString(ptr.getCellRef().getFaction(), "Faction"); + text += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); text += MWGui::ToolTips::getMiscString(ref->mBase->mScript, "Script"); } info.text = text; diff --git a/apps/openmw/mwclass/apparatus.hpp b/apps/openmw/mwclass/apparatus.hpp index 17b8b9254..5cdda8f26 100644 --- a/apps/openmw/mwclass/apparatus.hpp +++ b/apps/openmw/mwclass/apparatus.hpp @@ -13,6 +13,9 @@ namespace MWClass public: + /// Return ID of \a ptr + virtual std::string getId (const MWWorld::Ptr& ptr) const; + virtual float getWeight (const MWWorld::Ptr& ptr) const; virtual void insertObjectRendering (const MWWorld::Ptr& ptr, MWRender::RenderingInterface& renderingInterface) const; diff --git a/apps/openmw/mwclass/armor.cpp b/apps/openmw/mwclass/armor.cpp index 825b14978..97f9211d9 100644 --- a/apps/openmw/mwclass/armor.cpp +++ b/apps/openmw/mwclass/armor.cpp @@ -25,6 +25,11 @@ namespace MWClass { + std::string Armor::getId (const MWWorld::Ptr& ptr) const + { + return ptr.get()->mBase->mId; + } + void Armor::insertObjectRendering (const MWWorld::Ptr& ptr, MWRender::RenderingInterface& renderingInterface) const { const std::string model = getModel(ptr); @@ -152,15 +157,19 @@ namespace MWClass float iWeight = gmst.find (typeGmst)->getInt(); - if (iWeight * gmst.find ("fLightMaxMod")->getFloat()>= - ref->mBase->mData.mWeight) + float epsilon = 5e-4; + + if (ref->mBase->mData.mWeight == 0) + return ESM::Skill::Unarmored; + + if (ref->mBase->mData.mWeight <= iWeight * gmst.find ("fLightMaxMod")->getFloat() + epsilon) return ESM::Skill::LightArmor; - if (iWeight * gmst.find ("fMedMaxMod")->getFloat()>= - ref->mBase->mData.mWeight) + if (ref->mBase->mData.mWeight <= iWeight * gmst.find ("fMedMaxMod")->getFloat() + epsilon) return ESM::Skill::MediumArmor; - return ESM::Skill::HeavyArmor; + else + return ESM::Skill::HeavyArmor; } int Armor::getValue (const MWWorld::Ptr& ptr) const @@ -168,7 +177,7 @@ namespace MWClass MWWorld::LiveCellRef *ref = ptr.get(); - return ref->mBase->mData.mValue * (static_cast(getItemHealth(ptr)) / getItemMaxHealth(ptr)); + return ref->mBase->mData.mValue; } void Armor::registerSelf() @@ -244,11 +253,10 @@ namespace MWClass + MWGui::ToolTips::toString(ref->mBase->mData.mHealth); text += "\n#{sWeight}: " + MWGui::ToolTips::toString(ref->mBase->mData.mWeight) + " (" + typeText + ")"; - text += MWGui::ToolTips::getValueString(getValue(ptr), "#{sValue}"); + text += MWGui::ToolTips::getValueString(ref->mBase->mData.mValue, "#{sValue}"); if (MWBase::Environment::get().getWindowManager()->getFullHelp()) { - text += MWGui::ToolTips::getMiscString(ptr.getCellRef().getOwner(), "Owner"); - text += MWGui::ToolTips::getMiscString(ptr.getCellRef().getFaction(), "Faction"); + text += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); text += MWGui::ToolTips::getMiscString(ref->mBase->mScript, "Script"); } @@ -372,7 +380,8 @@ namespace MWClass bool Armor::canSell (const MWWorld::Ptr& item, int npcServices) const { - return npcServices & ESM::NPC::Armor; + return (npcServices & ESM::NPC::Armor) + || ((npcServices & ESM::NPC::MagicItems) && !getEnchantment(item).empty()); } float Armor::getWeight(const MWWorld::Ptr &ptr) const diff --git a/apps/openmw/mwclass/armor.hpp b/apps/openmw/mwclass/armor.hpp index e9164f920..8b7804c63 100644 --- a/apps/openmw/mwclass/armor.hpp +++ b/apps/openmw/mwclass/armor.hpp @@ -12,6 +12,9 @@ namespace MWClass public: + /// Return ID of \a ptr + virtual std::string getId (const MWWorld::Ptr& ptr) const; + virtual float getWeight (const MWWorld::Ptr& ptr) const; virtual void insertObjectRendering (const MWWorld::Ptr& ptr, MWRender::RenderingInterface& renderingInterface) const; diff --git a/apps/openmw/mwclass/book.cpp b/apps/openmw/mwclass/book.cpp index 0cc2e6020..51d47e721 100644 --- a/apps/openmw/mwclass/book.cpp +++ b/apps/openmw/mwclass/book.cpp @@ -22,6 +22,11 @@ namespace MWClass { + std::string Book::getId (const MWWorld::Ptr& ptr) const + { + return ptr.get()->mBase->mId; + } + void Book::insertObjectRendering (const MWWorld::Ptr& ptr, MWRender::RenderingInterface& renderingInterface) const { const std::string model = getModel(ptr); @@ -136,11 +141,10 @@ namespace MWClass std::string text; text += "\n#{sWeight}: " + MWGui::ToolTips::toString(ref->mBase->mData.mWeight); - text += MWGui::ToolTips::getValueString(getValue(ptr), "#{sValue}"); + text += MWGui::ToolTips::getValueString(ref->mBase->mData.mValue, "#{sValue}"); if (MWBase::Environment::get().getWindowManager()->getFullHelp()) { - text += MWGui::ToolTips::getMiscString(ptr.getCellRef().getOwner(), "Owner"); - text += MWGui::ToolTips::getMiscString(ptr.getCellRef().getFaction(), "Faction"); + text += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); text += MWGui::ToolTips::getMiscString(ref->mBase->mScript, "Script"); } @@ -198,7 +202,8 @@ namespace MWClass bool Book::canSell (const MWWorld::Ptr& item, int npcServices) const { - return npcServices & ESM::NPC::Books; + return (npcServices & ESM::NPC::Books) + || ((npcServices & ESM::NPC::MagicItems) && !getEnchantment(item).empty()); } float Book::getWeight(const MWWorld::Ptr &ptr) const diff --git a/apps/openmw/mwclass/book.hpp b/apps/openmw/mwclass/book.hpp index b60ef41d6..49d21e8bf 100644 --- a/apps/openmw/mwclass/book.hpp +++ b/apps/openmw/mwclass/book.hpp @@ -12,6 +12,9 @@ namespace MWClass public: + /// Return ID of \a ptr + virtual std::string getId (const MWWorld::Ptr& ptr) const; + virtual void insertObjectRendering (const MWWorld::Ptr& ptr, MWRender::RenderingInterface& renderingInterface) const; ///< Add reference into a cell for rendering diff --git a/apps/openmw/mwclass/clothing.cpp b/apps/openmw/mwclass/clothing.cpp index c0362188b..009878350 100644 --- a/apps/openmw/mwclass/clothing.cpp +++ b/apps/openmw/mwclass/clothing.cpp @@ -22,6 +22,11 @@ namespace MWClass { + std::string Clothing::getId (const MWWorld::Ptr& ptr) const + { + return ptr.get()->mBase->mId; + } + void Clothing::insertObjectRendering (const MWWorld::Ptr& ptr, MWRender::RenderingInterface& renderingInterface) const { const std::string model = getModel(ptr); @@ -190,11 +195,10 @@ namespace MWClass std::string text; text += "\n#{sWeight}: " + MWGui::ToolTips::toString(ref->mBase->mData.mWeight); - text += MWGui::ToolTips::getValueString(getValue(ptr), "#{sValue}"); + text += MWGui::ToolTips::getValueString(ref->mBase->mData.mValue, "#{sValue}"); if (MWBase::Environment::get().getWindowManager()->getFullHelp()) { - text += MWGui::ToolTips::getMiscString(ptr.getCellRef().getOwner(), "Owner"); - text += MWGui::ToolTips::getMiscString(ptr.getCellRef().getFaction(), "Faction"); + text += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); text += MWGui::ToolTips::getMiscString(ref->mBase->mScript, "Script"); } @@ -288,7 +292,8 @@ namespace MWClass bool Clothing::canSell (const MWWorld::Ptr& item, int npcServices) const { - return npcServices & ESM::NPC::Clothing; + return (npcServices & ESM::NPC::Clothing) + || ((npcServices & ESM::NPC::MagicItems) && !getEnchantment(item).empty()); } float Clothing::getWeight(const MWWorld::Ptr &ptr) const diff --git a/apps/openmw/mwclass/clothing.hpp b/apps/openmw/mwclass/clothing.hpp index 052928238..99ce61ece 100644 --- a/apps/openmw/mwclass/clothing.hpp +++ b/apps/openmw/mwclass/clothing.hpp @@ -12,6 +12,9 @@ namespace MWClass public: + /// Return ID of \a ptr + virtual std::string getId (const MWWorld::Ptr& ptr) const; + virtual void insertObjectRendering (const MWWorld::Ptr& ptr, MWRender::RenderingInterface& renderingInterface) const; ///< Add reference into a cell for rendering diff --git a/apps/openmw/mwclass/container.cpp b/apps/openmw/mwclass/container.cpp index 9498ea52d..179070aed 100644 --- a/apps/openmw/mwclass/container.cpp +++ b/apps/openmw/mwclass/container.cpp @@ -43,6 +43,11 @@ namespace namespace MWClass { + std::string Container::getId (const MWWorld::Ptr& ptr) const + { + return ptr.get()->mBase->mId; + } + void Container::ensureCustomData (const MWWorld::Ptr& ptr) const { if (!ptr.getRefData().getCustomData()) @@ -235,8 +240,7 @@ namespace MWClass text += "\n#{sTrapped}"; if (MWBase::Environment::get().getWindowManager()->getFullHelp()) { - text += MWGui::ToolTips::getMiscString(ptr.getCellRef().getOwner(), "Owner"); - text += MWGui::ToolTips::getMiscString(ptr.getCellRef().getFaction(), "Faction"); + text += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); text += MWGui::ToolTips::getMiscString(ref->mBase->mScript, "Script"); } @@ -286,7 +290,12 @@ namespace MWClass { const ESM::ContainerState& state2 = dynamic_cast (state); - ensureCustomData (ptr); + if (!ptr.getRefData().getCustomData()) + { + // Create a CustomData, but don't fill it from ESM records (not needed) + std::auto_ptr data (new ContainerCustomData); + ptr.getRefData().setCustomData (data.release()); + } dynamic_cast (*ptr.getRefData().getCustomData()).mContainerStore. readState (state2.mInventory); diff --git a/apps/openmw/mwclass/container.hpp b/apps/openmw/mwclass/container.hpp index 9fc013e45..e926a71fe 100644 --- a/apps/openmw/mwclass/container.hpp +++ b/apps/openmw/mwclass/container.hpp @@ -15,6 +15,9 @@ namespace MWClass public: + /// Return ID of \a ptr + virtual std::string getId (const MWWorld::Ptr& ptr) const; + virtual void insertObjectRendering (const MWWorld::Ptr& ptr, MWRender::RenderingInterface& renderingInterface) const; ///< Add reference into a cell for rendering diff --git a/apps/openmw/mwclass/creature.cpp b/apps/openmw/mwclass/creature.cpp index 1a6e4e321..5910c471b 100644 --- a/apps/openmw/mwclass/creature.cpp +++ b/apps/openmw/mwclass/creature.cpp @@ -9,6 +9,7 @@ #include "../mwmechanics/movement.hpp" #include "../mwmechanics/disease.hpp" #include "../mwmechanics/spellcasting.hpp" +#include "../mwmechanics/difficultyscaling.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" @@ -59,35 +60,38 @@ namespace namespace MWClass { + const Creature::GMST& Creature::getGmst() + { + static GMST gmst; + static bool inited = false; + if (!inited) + { + const MWBase::World *world = MWBase::Environment::get().getWorld(); + const MWWorld::Store &store = world->getStore().get(); + gmst.fMinWalkSpeedCreature = store.find("fMinWalkSpeedCreature"); + gmst.fMaxWalkSpeedCreature = store.find("fMaxWalkSpeedCreature"); + gmst.fEncumberedMoveEffect = store.find("fEncumberedMoveEffect"); + gmst.fSneakSpeedMultiplier = store.find("fSneakSpeedMultiplier"); + gmst.fAthleticsRunBonus = store.find("fAthleticsRunBonus"); + gmst.fBaseRunMultiplier = store.find("fBaseRunMultiplier"); + gmst.fMinFlySpeed = store.find("fMinFlySpeed"); + gmst.fMaxFlySpeed = store.find("fMaxFlySpeed"); + gmst.fSwimRunBase = store.find("fSwimRunBase"); + gmst.fSwimRunAthleticsMult = store.find("fSwimRunAthleticsMult"); + gmst.fKnockDownMult = store.find("fKnockDownMult"); + gmst.iKnockDownOddsMult = store.find("iKnockDownOddsMult"); + gmst.iKnockDownOddsBase = store.find("iKnockDownOddsBase"); + inited = true; + } + return gmst; + } + void Creature::ensureCustomData (const MWWorld::Ptr& ptr) const { if (!ptr.getRefData().getCustomData()) { std::auto_ptr data (new CreatureCustomData); - static bool inited = false; - if(!inited) - { - const MWBase::World *world = MWBase::Environment::get().getWorld(); - const MWWorld::Store &gmst = world->getStore().get(); - - fMinWalkSpeedCreature = gmst.find("fMinWalkSpeedCreature"); - fMaxWalkSpeedCreature = gmst.find("fMaxWalkSpeedCreature"); - fEncumberedMoveEffect = gmst.find("fEncumberedMoveEffect"); - fSneakSpeedMultiplier = gmst.find("fSneakSpeedMultiplier"); - fAthleticsRunBonus = gmst.find("fAthleticsRunBonus"); - fBaseRunMultiplier = gmst.find("fBaseRunMultiplier"); - fMinFlySpeed = gmst.find("fMinFlySpeed"); - fMaxFlySpeed = gmst.find("fMaxFlySpeed"); - fSwimRunBase = gmst.find("fSwimRunBase"); - fSwimRunAthleticsMult = gmst.find("fSwimRunAthleticsMult"); - fKnockDownMult = gmst.find("fKnockDownMult"); - iKnockDownOddsMult = gmst.find("iKnockDownOddsMult"); - iKnockDownOddsBase = gmst.find("iKnockDownOddsBase"); - - inited = true; - } - MWWorld::LiveCellRef *ref = ptr.get(); // creature stats @@ -125,6 +129,8 @@ namespace MWClass data->mCreatureStats.setGoldPool(ref->mBase->mData.mGold); + data->mCreatureStats.setNeedRecalcDynamicStats(false); + // store ptr.getRefData().setCustomData(data.release()); @@ -144,9 +150,9 @@ namespace MWClass return ref->mBase->mId; } - void Creature::adjustPosition(const MWWorld::Ptr& ptr) const + void Creature::adjustPosition(const MWWorld::Ptr& ptr, bool force) const { - MWBase::Environment::get().getWorld()->adjustPosition(ptr); + MWBase::Environment::get().getWorld()->adjustPosition(ptr, force); } void Creature::insertObjectRendering (const MWWorld::Ptr& ptr, MWRender::RenderingInterface& renderingInterface) const @@ -205,6 +211,9 @@ namespace MWClass const MWWorld::Store &gmst = MWBase::Environment::get().getWorld()->getStore().get(); MWMechanics::CreatureStats &stats = getCreatureStats(ptr); + if (stats.getDrawState() != MWMechanics::DrawState_Weapon) + return; + // Get the weapon used (if hand-to-hand, weapon = inv.end()) MWWorld::Ptr weapon; if (ptr.getClass().hasInventoryStore(ptr)) @@ -221,7 +230,7 @@ namespace MWClass const float fFatigueAttackMult = gmst.find("fFatigueAttackMult")->getFloat(); const float fWeaponFatigueMult = gmst.find("fWeaponFatigueMult")->getFloat(); MWMechanics::DynamicStat fatigue = stats.getFatigue(); - const float normalizedEncumbrance = getEncumbrance(ptr) / getCapacity(ptr); + const float normalizedEncumbrance = getNormalizedEncumbrance(ptr); float fatigueLoss = fFatigueAttackBase + normalizedEncumbrance * fFatigueAttackMult; if (!weapon.isEmpty()) fatigueLoss += weapon.getClass().getWeight(weapon) * stats.getAttackStrength() * fWeaponFatigueMult; @@ -251,6 +260,7 @@ namespace MWClass if((::rand()/(RAND_MAX+1.0)) > hitchance/100.0f) { victim.getClass().onHit(victim, 0.0f, false, MWWorld::Ptr(), ptr, false); + MWMechanics::reduceWeaponCondition(0.f, false, weapon, ptr); return; } @@ -272,12 +282,10 @@ namespace MWClass break; } - // I think this should be random, since attack1-3 animations don't have an attack strength like NPCs do - float damage = min + (max - min) * ::rand()/(RAND_MAX+1.0); + float damage = min + (max - min) * stats.getAttackStrength(); if (!weapon.isEmpty()) { - const bool weaphashealth = weapon.getClass().hasItemHealth(weapon); const unsigned char *attack = NULL; if(type == ESM::Weapon::AT_Chop) attack = weapon.get()->mBase->mData.mChop; @@ -287,29 +295,11 @@ namespace MWClass attack = weapon.get()->mBase->mData.mThrust; if(attack) { - float weaponDamage = attack[0] + ((attack[1]-attack[0])*stats.getAttackStrength()); - weaponDamage *= 0.5f + (stats.getAttribute(ESM::Attribute::Luck).getModified() / 100.0f); - if(weaphashealth) - { - int weapmaxhealth = weapon.getClass().getItemMaxHealth(weapon); - int weaphealth = weapon.getClass().getItemHealth(weapon); - weaponDamage *= float(weaphealth) / weapmaxhealth; - - if (!MWBase::Environment::get().getWorld()->getGodModeState()) - { - // Reduce weapon charge by at least one, but cap at 0 - weaphealth -= std::min(std::max(1, - (int)(damage * gmst.find("fWeaponDamageMult")->getFloat())), weaphealth); - - weapon.getCellRef().setCharge(weaphealth); - } - - // Weapon broken? unequip it - if (weapon.getCellRef().getCharge() == 0) - weapon = *getInventoryStore(ptr).unequipItem(weapon, ptr); - } - - damage += weaponDamage; + damage = attack[0] + ((attack[1]-attack[0])*stats.getAttackStrength()); + damage *= gmst.find("fDamageStrengthBase")->getFloat() + + (stats.getAttribute(ESM::Attribute::Strength).getModified() * gmst.find("fDamageStrengthMult")->getFloat() * 0.1); + MWMechanics::adjustWeaponDamage(damage, weapon); + MWMechanics::reduceWeaponCondition(damage, true, weapon, ptr); } // Apply "On hit" enchanted weapons @@ -327,6 +317,8 @@ namespace MWClass } } + MWMechanics::applyElementalShields(ptr, victim); + if (!weapon.isEmpty() && MWMechanics::blockMeleeAttack(ptr, victim, weapon, damage)) damage = 0; @@ -342,11 +334,17 @@ namespace MWClass { // NOTE: 'object' and/or 'attacker' may be empty. - getCreatureStats(ptr).setAttacked(true); + if (!attacker.isEmpty() && !ptr.getClass().getCreatureStats(ptr).getAiSequence().isInCombat(attacker)) + getCreatureStats(ptr).setAttacked(true); // Self defense - if (!attacker.isEmpty() && ptr.getClass().getCreatureStats(ptr).getAiSetting(MWMechanics::CreatureStats::AI_Fight).getModified() < 80) - MWBase::Environment::get().getMechanicsManager()->startCombat(ptr, attacker); + bool setOnPcHitMe = true; // Note OnPcHitMe is not set for friendly hits. + if ((canWalk(ptr) || canFly(ptr) || canSwim(ptr)) // No retaliation for totally static creatures + // (they have no movement or attacks anyway) + && !attacker.isEmpty()) + { + setOnPcHitMe = MWBase::Environment::get().getMechanicsManager()->actorAttacked(ptr, attacker); + } if(!successful) { @@ -360,7 +358,7 @@ namespace MWClass if(!object.isEmpty()) getCreatureStats(ptr).setLastHitObject(object.getClass().getId(object)); - if(!attacker.isEmpty() && attacker.getRefData().getHandle() == "player") + if(setOnPcHitMe && !attacker.isEmpty() && attacker.getRefData().getHandle() == "player") { const std::string &script = ptr.get()->mBase->mScript; /* Set the OnPCHitMe script variable. The script is responsible for clearing it. */ @@ -371,23 +369,34 @@ namespace MWClass if (damage > 0.0f && !object.isEmpty()) MWMechanics::resistNormalWeapon(ptr, attacker, object, damage); + if (damage < 0.001f) + damage = 0; + if (damage > 0.f) { - // Check for knockdown - float agilityTerm = getCreatureStats(ptr).getAttribute(ESM::Attribute::Agility).getModified() * fKnockDownMult->getFloat(); - float knockdownTerm = getCreatureStats(ptr).getAttribute(ESM::Attribute::Agility).getModified() - * iKnockDownOddsMult->getInt() * 0.01 + iKnockDownOddsBase->getInt(); - int roll = std::rand()/ (static_cast (RAND_MAX) + 1) * 100; // [0, 99] - if (ishealth && agilityTerm <= damage && knockdownTerm <= roll) + if (!attacker.isEmpty()) { - getCreatureStats(ptr).setKnockedDown(true); + // Check for knockdown + float agilityTerm = getCreatureStats(ptr).getAttribute(ESM::Attribute::Agility).getModified() * getGmst().fKnockDownMult->getFloat(); + float knockdownTerm = getCreatureStats(ptr).getAttribute(ESM::Attribute::Agility).getModified() + * getGmst().iKnockDownOddsMult->getInt() * 0.01 + getGmst().iKnockDownOddsBase->getInt(); + int roll = std::rand()/ (static_cast (RAND_MAX) + 1) * 100; // [0, 99] + if (ishealth && agilityTerm <= damage && knockdownTerm <= roll) + { + getCreatureStats(ptr).setKnockedDown(true); + } + else + getCreatureStats(ptr).setHitRecovery(true); // Is this supposed to always occur? } - else - getCreatureStats(ptr).setHitRecovery(true); // Is this supposed to always occur? + + damage = std::max(1.f, damage); if(ishealth) { + if (!attacker.isEmpty()) + damage = scaleDamage(damage, attacker, ptr); + MWBase::Environment::get().getSoundManager()->playSound3D(ptr, "Health Damage", 1.0f, 1.0f); float health = getCreatureStats(ptr).getHealth().getCurrent() - damage; setActorHealth(ptr, health, attacker); @@ -461,6 +470,8 @@ namespace MWClass if(getCreatureStats(ptr).isDead()) return boost::shared_ptr(new MWWorld::ActionOpen(ptr, true)); + if(ptr.getClass().getCreatureStats(ptr).getAiSequence().isInCombat()) + return boost::shared_ptr(new MWWorld::FailedAction("")); return boost::shared_ptr(new MWWorld::ActionTalk(ptr)); } @@ -512,38 +523,37 @@ namespace MWClass bool Creature::hasToolTip (const MWWorld::Ptr& ptr) const { - /// \todo We don't want tooltips for Creatures in combat mode. - - return true; + return !ptr.getClass().getCreatureStats(ptr).getAiSequence().isInCombat() || getCreatureStats(ptr).isDead(); } float Creature::getSpeed(const MWWorld::Ptr &ptr) const { MWMechanics::CreatureStats& stats = getCreatureStats(ptr); + const GMST& gmst = getGmst(); - float walkSpeed = fMinWalkSpeedCreature->getFloat() + 0.01 * stats.getAttribute(ESM::Attribute::Speed).getModified() - * (fMaxWalkSpeedCreature->getFloat() - fMinWalkSpeedCreature->getFloat()); + float walkSpeed = gmst.fMinWalkSpeedCreature->getFloat() + 0.01 * stats.getAttribute(ESM::Attribute::Speed).getModified() + * (gmst.fMaxWalkSpeedCreature->getFloat() - gmst.fMinWalkSpeedCreature->getFloat()); const MWBase::World *world = MWBase::Environment::get().getWorld(); const MWMechanics::MagicEffects &mageffects = stats.getMagicEffects(); - const float normalizedEncumbrance = getEncumbrance(ptr) / getCapacity(ptr); - bool running = ptr.getClass().getCreatureStats(ptr).getStance(MWMechanics::CreatureStats::Stance_Run); - float runSpeed = walkSpeed*(0.01f * getSkill(ptr, ESM::Skill::Athletics) * - fAthleticsRunBonus->getFloat() + fBaseRunMultiplier->getFloat()); + // The Run speed difference for creatures comes from the animation speed difference (see runStateToWalkState in character.cpp) + float runSpeed = walkSpeed; float moveSpeed; - if(normalizedEncumbrance >= 1.0f) + + if(getEncumbrance(ptr) > getCapacity(ptr)) moveSpeed = 0.0f; - else if(canFly(ptr) || (mageffects.get(ESM::MagicEffect::Levitate).mMagnitude > 0 && + else if(canFly(ptr) || (mageffects.get(ESM::MagicEffect::Levitate).getMagnitude() > 0 && world->isLevitationEnabled())) { float flySpeed = 0.01f*(stats.getAttribute(ESM::Attribute::Speed).getModified() + - mageffects.get(ESM::MagicEffect::Levitate).mMagnitude); - flySpeed = fMinFlySpeed->getFloat() + flySpeed*(fMaxFlySpeed->getFloat() - fMinFlySpeed->getFloat()); - flySpeed *= 1.0f - fEncumberedMoveEffect->getFloat() * normalizedEncumbrance; + mageffects.get(ESM::MagicEffect::Levitate).getMagnitude()); + flySpeed = gmst.fMinFlySpeed->getFloat() + flySpeed*(gmst.fMaxFlySpeed->getFloat() - gmst.fMinFlySpeed->getFloat()); + const float normalizedEncumbrance = getNormalizedEncumbrance(ptr); + flySpeed *= 1.0f - gmst.fEncumberedMoveEffect->getFloat() * normalizedEncumbrance; flySpeed = std::max(0.0f, flySpeed); moveSpeed = flySpeed; } @@ -552,9 +562,9 @@ namespace MWClass float swimSpeed = walkSpeed; if(running) swimSpeed = runSpeed; - swimSpeed *= 1.0f + 0.01f * mageffects.get(ESM::MagicEffect::SwiftSwim).mMagnitude; - swimSpeed *= fSwimRunBase->getFloat() + 0.01f*getSkill(ptr, ESM::Skill::Athletics) * - fSwimRunAthleticsMult->getFloat(); + swimSpeed *= 1.0f + 0.01f * mageffects.get(ESM::MagicEffect::SwiftSwim).getMagnitude(); + swimSpeed *= gmst.fSwimRunBase->getFloat() + 0.01f*getSkill(ptr, ESM::Skill::Athletics) * + gmst.fSwimRunAthleticsMult->getFloat(); moveSpeed = swimSpeed; } else if(running) @@ -612,8 +622,8 @@ namespace MWClass float Creature::getArmorRating (const MWWorld::Ptr& ptr) const { - /// \todo add Shield magic effect magnitude here, controlled by a GMST (Vanilla vs MCP behaviour) - return 0.f; + // Note this is currently unused. Creatures do not use armor mitigation. + return getCreatureStats(ptr).getMagicEffects().get(ESM::MagicEffect::Shield).getMagnitude(); } float Creature::getCapacity (const MWWorld::Ptr& ptr) const @@ -628,9 +638,9 @@ namespace MWClass const MWMechanics::CreatureStats& stats = getCreatureStats (ptr); - weight -= stats.getMagicEffects().get (MWMechanics::EffectKey (ESM::MagicEffect::Feather)).mMagnitude; + weight -= stats.getMagicEffects().get (MWMechanics::EffectKey (ESM::MagicEffect::Feather)).getMagnitude(); - weight += stats.getMagicEffects().get (MWMechanics::EffectKey (ESM::MagicEffect::Burden)).mMagnitude; + weight += stats.getMagicEffects().get (MWMechanics::EffectKey (ESM::MagicEffect::Burden)).getMagnitude(); if (weight<0) weight = 0; @@ -711,7 +721,7 @@ namespace MWClass MWWorld::LiveCellRef *ref = ptr.get(); - return ref->mBase->mFlags & ESM::Creature::Swims; + return ref->mBase->mFlags & ESM::Creature::Swims || ref->mBase->mFlags & ESM::Creature::Bipedal; } bool Creature::canWalk(const MWWorld::Ptr &ptr) const @@ -719,7 +729,7 @@ namespace MWClass MWWorld::LiveCellRef *ref = ptr.get(); - return ref->mBase->mFlags & ESM::Creature::Walks; + return ref->mBase->mFlags & ESM::Creature::Walks || ref->mBase->mFlags & ESM::Creature::Bipedal; } int Creature::getSndGenTypeFromName(const MWWorld::Ptr &ptr, const std::string &name) @@ -728,7 +738,7 @@ namespace MWClass { MWBase::World *world = MWBase::Environment::get().getWorld(); Ogre::Vector3 pos(ptr.getRefData().getPosition().pos); - if(world->isUnderwater(ptr.getCell(), pos)) + if(world->isUnderwater(ptr.getCell(), pos) || world->isWalkingOnWater(ptr)) return 2; if(world->isOnGround(ptr)) return 0; @@ -738,7 +748,7 @@ namespace MWClass { MWBase::World *world = MWBase::Environment::get().getWorld(); Ogre::Vector3 pos(ptr.getRefData().getPosition().pos); - if(world->isUnderwater(ptr.getCell(), pos)) + if(world->isUnderwater(ptr.getCell(), pos) || world->isWalkingOnWater(ptr)) return 3; if(world->isOnGround(ptr)) return 1; @@ -796,7 +806,27 @@ namespace MWClass { const ESM::CreatureState& state2 = dynamic_cast (state); - ensureCustomData (ptr); + ensureCustomData(ptr); + + // If we do the following instead we get a sizable speedup, but this causes compatibility issues + // with 0.30 savegames, where some state in CreatureStats was not saved yet, + // and therefore needs to be loaded from ESM records. TODO: re-enable this in a future release. + /* + if (!ptr.getRefData().getCustomData()) + { + // Create a CustomData, but don't fill it from ESM records (not needed) + std::auto_ptr data (new CreatureCustomData); + + MWWorld::LiveCellRef *ref = ptr.get(); + + if (ref->mBase->mFlags & ESM::Creature::Weapon) + data->mContainerStore = new MWWorld::InventoryStore(); + else + data->mContainerStore = new MWWorld::ContainerStore(); + + ptr.getRefData().setCustomData (data.release()); + } + */ CreatureCustomData& customData = dynamic_cast (*ptr.getRefData().getCustomData()); @@ -835,8 +865,7 @@ namespace MWClass ptr.getRefData().setCount(1); // Reset to original position - ESM::Position& pos = ptr.getRefData().getPosition(); - pos = ptr.getCellRef().getPosition(); + ptr.getRefData().setPosition(ptr.getCellRef().getPosition()); ptr.getRefData().setCustomData(NULL); } @@ -850,19 +879,4 @@ namespace MWClass MWWorld::ContainerStore& store = getContainerStore(ptr); store.restock(list, ptr, ptr.getCellRef().getRefId(), ptr.getCellRef().getFaction()); } - - const ESM::GameSetting* Creature::fMinWalkSpeedCreature; - const ESM::GameSetting* Creature::fMaxWalkSpeedCreature; - const ESM::GameSetting *Creature::fEncumberedMoveEffect; - const ESM::GameSetting *Creature::fSneakSpeedMultiplier; - const ESM::GameSetting *Creature::fAthleticsRunBonus; - const ESM::GameSetting *Creature::fBaseRunMultiplier; - const ESM::GameSetting *Creature::fMinFlySpeed; - const ESM::GameSetting *Creature::fMaxFlySpeed; - const ESM::GameSetting *Creature::fSwimRunBase; - const ESM::GameSetting *Creature::fSwimRunAthleticsMult; - const ESM::GameSetting *Creature::fKnockDownMult; - const ESM::GameSetting *Creature::iKnockDownOddsMult; - const ESM::GameSetting *Creature::iKnockDownOddsBase; - } diff --git a/apps/openmw/mwclass/creature.hpp b/apps/openmw/mwclass/creature.hpp index 30573cd15..1820d4ea4 100644 --- a/apps/openmw/mwclass/creature.hpp +++ b/apps/openmw/mwclass/creature.hpp @@ -19,20 +19,25 @@ namespace MWClass static int getSndGenTypeFromName(const MWWorld::Ptr &ptr, const std::string &name); - static const ESM::GameSetting *fMinWalkSpeedCreature; - static const ESM::GameSetting *fMaxWalkSpeedCreature; - static const ESM::GameSetting *fEncumberedMoveEffect; - static const ESM::GameSetting *fSneakSpeedMultiplier; - static const ESM::GameSetting *fAthleticsRunBonus; - static const ESM::GameSetting *fBaseRunMultiplier; - static const ESM::GameSetting *fMinFlySpeed; - static const ESM::GameSetting *fMaxFlySpeed; - static const ESM::GameSetting *fSwimRunBase; - static const ESM::GameSetting *fSwimRunAthleticsMult; - static const ESM::GameSetting *fKnockDownMult; - static const ESM::GameSetting *iKnockDownOddsMult; - static const ESM::GameSetting *iKnockDownOddsBase; - + // cached GMSTs + struct GMST + { + const ESM::GameSetting *fMinWalkSpeedCreature; + const ESM::GameSetting *fMaxWalkSpeedCreature; + const ESM::GameSetting *fEncumberedMoveEffect; + const ESM::GameSetting *fSneakSpeedMultiplier; + const ESM::GameSetting *fAthleticsRunBonus; + const ESM::GameSetting *fBaseRunMultiplier; + const ESM::GameSetting *fMinFlySpeed; + const ESM::GameSetting *fMaxFlySpeed; + const ESM::GameSetting *fSwimRunBase; + const ESM::GameSetting *fSwimRunAthleticsMult; + const ESM::GameSetting *fKnockDownMult; + const ESM::GameSetting *iKnockDownOddsMult; + const ESM::GameSetting *iKnockDownOddsBase; + }; + + static const GMST& getGmst(); public: @@ -44,7 +49,9 @@ namespace MWClass virtual void insertObject(const MWWorld::Ptr& ptr, MWWorld::PhysicsSystem& physics) const; - virtual void adjustPosition(const MWWorld::Ptr& ptr) const; + virtual void adjustPosition(const MWWorld::Ptr& ptr, bool force) const; + ///< Adjust position to stand on ground. Must be called post model load + /// @param force do this even if the ptr is flying virtual std::string getName (const MWWorld::Ptr& ptr) const; ///< \return name (the one that is to be presented to the user; not the internal one); diff --git a/apps/openmw/mwclass/creaturelevlist.cpp b/apps/openmw/mwclass/creaturelevlist.cpp index 784304804..b1e27c345 100644 --- a/apps/openmw/mwclass/creaturelevlist.cpp +++ b/apps/openmw/mwclass/creaturelevlist.cpp @@ -27,6 +27,11 @@ namespace namespace MWClass { + std::string CreatureLevList::getId (const MWWorld::Ptr& ptr) const + { + return ptr.get()->mBase->mId; + } + std::string CreatureLevList::getName (const MWWorld::Ptr& ptr) const { return ""; @@ -78,6 +83,8 @@ namespace MWClass customData.mSpawnActorId = placed.getClass().getCreatureStats(placed).getActorId(); customData.mSpawn = false; } + else + customData.mSpawn = false; } void CreatureLevList::ensureCustomData(const MWWorld::Ptr &ptr) const diff --git a/apps/openmw/mwclass/creaturelevlist.hpp b/apps/openmw/mwclass/creaturelevlist.hpp index 6c51a3189..7016524eb 100644 --- a/apps/openmw/mwclass/creaturelevlist.hpp +++ b/apps/openmw/mwclass/creaturelevlist.hpp @@ -11,6 +11,9 @@ namespace MWClass public: + /// Return ID of \a ptr + virtual std::string getId (const MWWorld::Ptr& ptr) const; + virtual std::string getName (const MWWorld::Ptr& ptr) const; ///< \return name (the one that is to be presented to the user; not the internal one); /// can return an empty string. diff --git a/apps/openmw/mwclass/door.cpp b/apps/openmw/mwclass/door.cpp index 12645c9f3..fa9db9e16 100644 --- a/apps/openmw/mwclass/door.cpp +++ b/apps/openmw/mwclass/door.cpp @@ -42,6 +42,11 @@ namespace namespace MWClass { + std::string Door::getId (const MWWorld::Ptr& ptr) const + { + return ptr.get()->mBase->mId; + } + void Door::insertObjectRendering (const MWWorld::Ptr& ptr, MWRender::RenderingInterface& renderingInterface) const { const std::string model = getModel(ptr); @@ -57,11 +62,13 @@ namespace MWClass physics.addObject(ptr); // Resume the door's opening/closing animation if it wasn't finished - ensureCustomData(ptr); - const DoorCustomData& customData = dynamic_cast(*ptr.getRefData().getCustomData()); - if (customData.mDoorState > 0) + if (ptr.getRefData().getCustomData()) { - MWBase::Environment::get().getWorld()->activateDoor(ptr, customData.mDoorState == 1 ? true : false); + const DoorCustomData& customData = dynamic_cast(*ptr.getRefData().getCustomData()); + if (customData.mDoorState > 0) + { + MWBase::Environment::get().getWorld()->activateDoor(ptr, customData.mDoorState); + } } } @@ -125,7 +132,7 @@ namespace MWClass MWBase::Environment::get().getWindowManager()->messageBox(keyName + " #{sKeyUsed}"); unlock(ptr); //Call the function here. because that makes sense. // using a key disarms the trap - ptr.getCellRef().getTrap() = ""; + ptr.getCellRef().setTrap(""); } if (!needKey || hasKey) @@ -248,8 +255,10 @@ namespace MWClass text += "\n#{sTrapped}"; if (MWBase::Environment::get().getWindowManager()->getFullHelp()) + { + text += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); text += MWGui::ToolTips::getMiscString(ref->mBase->mScript, "Script"); - + } info.text = text; return info; @@ -315,6 +324,9 @@ namespace MWClass void Door::setDoorState (const MWWorld::Ptr &ptr, int state) const { + if (ptr.getCellRef().getTeleport()) + throw std::runtime_error("load doors can't be moved"); + ensureCustomData(ptr); DoorCustomData& customData = dynamic_cast(*ptr.getRefData().getCustomData()); customData.mDoorState = state; diff --git a/apps/openmw/mwclass/door.hpp b/apps/openmw/mwclass/door.hpp index 12b360aa8..23e11d336 100644 --- a/apps/openmw/mwclass/door.hpp +++ b/apps/openmw/mwclass/door.hpp @@ -16,6 +16,9 @@ namespace MWClass public: + /// Return ID of \a ptr + virtual std::string getId (const MWWorld::Ptr& ptr) const; + virtual void insertObjectRendering (const MWWorld::Ptr& ptr, MWRender::RenderingInterface& renderingInterface) const; ///< Add reference into a cell for rendering diff --git a/apps/openmw/mwclass/ingredient.cpp b/apps/openmw/mwclass/ingredient.cpp index 60c0efeb8..fa03f23ff 100644 --- a/apps/openmw/mwclass/ingredient.cpp +++ b/apps/openmw/mwclass/ingredient.cpp @@ -144,11 +144,10 @@ namespace MWClass std::string text; text += "\n#{sWeight}: " + MWGui::ToolTips::toString(ref->mBase->mData.mWeight); - text += MWGui::ToolTips::getValueString(getValue(ptr), "#{sValue}"); + text += MWGui::ToolTips::getValueString(ref->mBase->mData.mValue, "#{sValue}"); if (MWBase::Environment::get().getWindowManager()->getFullHelp()) { - text += MWGui::ToolTips::getMiscString(ptr.getCellRef().getOwner(), "Owner"); - text += MWGui::ToolTips::getMiscString(ptr.getCellRef().getFaction(), "Faction"); + text += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); text += MWGui::ToolTips::getMiscString(ref->mBase->mScript, "Script"); } diff --git a/apps/openmw/mwclass/itemlevlist.cpp b/apps/openmw/mwclass/itemlevlist.cpp index 6ed9ab2e5..d31080bb2 100644 --- a/apps/openmw/mwclass/itemlevlist.cpp +++ b/apps/openmw/mwclass/itemlevlist.cpp @@ -5,6 +5,11 @@ namespace MWClass { + std::string ItemLevList::getId (const MWWorld::Ptr& ptr) const + { + return ptr.get()->mBase->mId; + } + std::string ItemLevList::getName (const MWWorld::Ptr& ptr) const { return ""; diff --git a/apps/openmw/mwclass/itemlevlist.hpp b/apps/openmw/mwclass/itemlevlist.hpp index 0b71b072c..2b507135f 100644 --- a/apps/openmw/mwclass/itemlevlist.hpp +++ b/apps/openmw/mwclass/itemlevlist.hpp @@ -9,6 +9,9 @@ namespace MWClass { public: + /// Return ID of \a ptr + virtual std::string getId (const MWWorld::Ptr& ptr) const; + virtual std::string getName (const MWWorld::Ptr& ptr) const; ///< \return name (the one that is to be presented to the user; not the internal one); /// can return an empty string. diff --git a/apps/openmw/mwclass/light.cpp b/apps/openmw/mwclass/light.cpp index fd45ec859..e6d266de2 100644 --- a/apps/openmw/mwclass/light.cpp +++ b/apps/openmw/mwclass/light.cpp @@ -47,12 +47,20 @@ namespace namespace MWClass { + std::string Light::getId (const MWWorld::Ptr& ptr) const + { + return ptr.get()->mBase->mId; + } + void Light::insertObjectRendering (const MWWorld::Ptr& ptr, MWRender::RenderingInterface& renderingInterface) const { const std::string model = getModel(ptr); - if(!model.empty()) { - renderingInterface.getObjects().insertModel(ptr, model); - } + + MWWorld::LiveCellRef *ref = + ptr.get(); + + // Insert even if model is empty, so that the light is added + renderingInterface.getObjects().insertModel(ptr, model, false, !(ref->mBase->mData.mFlags & ESM::Light::OffDefault)); } void Light::insertObject(const MWWorld::Ptr& ptr, MWWorld::PhysicsSystem& physics) const @@ -66,7 +74,7 @@ namespace MWClass if(!model.empty()) physics.addObject(ptr,ref->mBase->mData.mFlags & ESM::Light::Carry); - if (!ref->mBase->mSound.empty()) + if (!ref->mBase->mSound.empty() && !(ref->mBase->mData.mFlags & ESM::Light::OffDefault)) MWBase::Environment::get().getSoundManager()->playSound3D(ptr, ref->mBase->mSound, 1.0, 1.0, MWBase::SoundManager::Play_TypeSfx, MWBase::SoundManager::Play_Loop); @@ -183,12 +191,14 @@ namespace MWClass std::string text; - text += "\n#{sWeight}: " + MWGui::ToolTips::toString(ref->mBase->mData.mWeight); - text += MWGui::ToolTips::getValueString(getValue(ptr), "#{sValue}"); + if (ref->mBase->mData.mWeight != 0) + { + text += "\n#{sWeight}: " + MWGui::ToolTips::toString(ref->mBase->mData.mWeight); + text += MWGui::ToolTips::getValueString(ref->mBase->mData.mValue, "#{sValue}"); + } if (MWBase::Environment::get().getWindowManager()->getFullHelp()) { - text += MWGui::ToolTips::getMiscString(ptr.getCellRef().getOwner(), "Owner"); - text += MWGui::ToolTips::getMiscString(ptr.getCellRef().getFaction(), "Faction"); + text += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); text += MWGui::ToolTips::getMiscString(ref->mBase->mScript, "Script"); } @@ -290,4 +300,9 @@ namespace MWClass state2.mTime = dynamic_cast (*ptr.getRefData().getCustomData()).mTime; } + + std::string Light::getSound(const MWWorld::Ptr& ptr) const + { + return ptr.get()->mBase->mSound; + } } diff --git a/apps/openmw/mwclass/light.hpp b/apps/openmw/mwclass/light.hpp index 5568e1727..a3b841261 100644 --- a/apps/openmw/mwclass/light.hpp +++ b/apps/openmw/mwclass/light.hpp @@ -14,6 +14,9 @@ namespace MWClass public: + /// Return ID of \a ptr + virtual std::string getId (const MWWorld::Ptr& ptr) const; + virtual void insertObjectRendering (const MWWorld::Ptr& ptr, MWRender::RenderingInterface& renderingInterface) const; ///< Add reference into a cell for rendering @@ -79,6 +82,8 @@ namespace MWClass virtual void writeAdditionalState (const MWWorld::Ptr& ptr, ESM::ObjectState& state) const; ///< Write additional state from \a ptr into \a state. + + virtual std::string getSound(const MWWorld::Ptr& ptr) const; }; } diff --git a/apps/openmw/mwclass/lockpick.cpp b/apps/openmw/mwclass/lockpick.cpp index 19381a3fd..9df587024 100644 --- a/apps/openmw/mwclass/lockpick.cpp +++ b/apps/openmw/mwclass/lockpick.cpp @@ -22,6 +22,11 @@ namespace MWClass { + std::string Lockpick::getId (const MWWorld::Ptr& ptr) const + { + return ptr.get()->mBase->mId; + } + void Lockpick::insertObjectRendering (const MWWorld::Ptr& ptr, MWRender::RenderingInterface& renderingInterface) const { const std::string model = getModel(ptr); @@ -86,7 +91,7 @@ namespace MWClass MWWorld::LiveCellRef *ref = ptr.get(); - return ref->mBase->mData.mValue * (static_cast(getItemHealth(ptr)) / getItemMaxHealth(ptr)); + return ref->mBase->mData.mValue; } void Lockpick::registerSelf() @@ -138,11 +143,10 @@ namespace MWClass text += "\n#{sUses}: " + MWGui::ToolTips::toString(remainingUses); text += "\n#{sQuality}: " + MWGui::ToolTips::toString(ref->mBase->mData.mQuality); text += "\n#{sWeight}: " + MWGui::ToolTips::toString(ref->mBase->mData.mWeight); - text += MWGui::ToolTips::getValueString(getValue(ptr), "#{sValue}"); + text += MWGui::ToolTips::getValueString(ref->mBase->mData.mValue, "#{sValue}"); if (MWBase::Environment::get().getWindowManager()->getFullHelp()) { - text += MWGui::ToolTips::getMiscString(ptr.getCellRef().getOwner(), "Owner"); - text += MWGui::ToolTips::getMiscString(ptr.getCellRef().getFaction(), "Faction"); + text += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); text += MWGui::ToolTips::getMiscString(ref->mBase->mScript, "Script"); } diff --git a/apps/openmw/mwclass/lockpick.hpp b/apps/openmw/mwclass/lockpick.hpp index a7cf3791f..d4bdf3fa6 100644 --- a/apps/openmw/mwclass/lockpick.hpp +++ b/apps/openmw/mwclass/lockpick.hpp @@ -12,6 +12,9 @@ namespace MWClass public: + /// Return ID of \a ptr + virtual std::string getId (const MWWorld::Ptr& ptr) const; + virtual void insertObjectRendering (const MWWorld::Ptr& ptr, MWRender::RenderingInterface& renderingInterface) const; ///< Add reference into a cell for rendering @@ -64,6 +67,9 @@ namespace MWClass virtual int getItemMaxHealth (const MWWorld::Ptr& ptr) const; ///< Return item max health or throw an exception, if class does not have item health + + virtual bool hasItemHealth (const MWWorld::Ptr& ptr) const { return true; } + ///< \return Item health data available? (default implementation: false) }; } diff --git a/apps/openmw/mwclass/misc.cpp b/apps/openmw/mwclass/misc.cpp index 1044fb01d..1b4719c6e 100644 --- a/apps/openmw/mwclass/misc.cpp +++ b/apps/openmw/mwclass/misc.cpp @@ -38,6 +38,11 @@ bool isGold (const MWWorld::Ptr& ptr) namespace MWClass { + std::string Miscellaneous::getId (const MWWorld::Ptr& ptr) const + { + return ptr.get()->mBase->mId; + } + void Miscellaneous::insertObjectRendering (const MWWorld::Ptr& ptr, MWRender::RenderingInterface& renderingInterface) const { const std::string model = getModel(ptr); @@ -175,15 +180,14 @@ namespace MWClass std::string text; - if (!gold) + if (!gold && !ref->mBase->mData.mIsKey) { text += "\n#{sWeight}: " + MWGui::ToolTips::toString(ref->mBase->mData.mWeight); text += MWGui::ToolTips::getValueString(getValue(ptr), "#{sValue}"); } if (MWBase::Environment::get().getWindowManager()->getFullHelp()) { - text += MWGui::ToolTips::getMiscString(ptr.getCellRef().getOwner(), "Owner"); - text += MWGui::ToolTips::getMiscString(ptr.getCellRef().getFaction(), "Faction"); + text += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); text += MWGui::ToolTips::getMiscString(ref->mBase->mScript, "Script"); } diff --git a/apps/openmw/mwclass/misc.hpp b/apps/openmw/mwclass/misc.hpp index 16e9ca10b..53a8e050b 100644 --- a/apps/openmw/mwclass/misc.hpp +++ b/apps/openmw/mwclass/misc.hpp @@ -12,6 +12,9 @@ namespace MWClass public: + /// Return ID of \a ptr + virtual std::string getId (const MWWorld::Ptr& ptr) const; + virtual void insertObjectRendering (const MWWorld::Ptr& ptr, MWRender::RenderingInterface& renderingInterface) const; ///< Add reference into a cell for rendering diff --git a/apps/openmw/mwclass/npc.cpp b/apps/openmw/mwclass/npc.cpp index 7405292b4..d3f86c03b 100644 --- a/apps/openmw/mwclass/npc.cpp +++ b/apps/openmw/mwclass/npc.cpp @@ -22,6 +22,9 @@ #include "../mwmechanics/spellcasting.hpp" #include "../mwmechanics/disease.hpp" #include "../mwmechanics/combat.hpp" +#include "../mwmechanics/autocalcspell.hpp" +#include "../mwmechanics/difficultyscaling.hpp" +#include "../mwmechanics/character.hpp" #include "../mwworld/ptr.hpp" #include "../mwworld/actiontalk.hpp" @@ -53,6 +56,24 @@ namespace return new NpcCustomData (*this); } + int is_even(double d) { + double int_part; + modf(d / 2.0, &int_part); + return 2.0 * int_part == d; + } + + int round_ieee_754(double d) { + double i = floor(d); + d -= i; + if(d < 0.5) + return i; + if(d > 0.5) + return i + 1.0; + if(is_even(i)) + return i; + return i + 1.0; + } + void autoCalculateAttributes (const ESM::NPC* npc, MWMechanics::CreatureStats& creatureStats) { // race bonus @@ -108,8 +129,9 @@ namespace } modifierSum += add; } - creatureStats.setAttribute(attribute, std::min(creatureStats.getAttribute(attribute).getBase() - + static_cast((level-1) * modifierSum+0.5), 100) ); + creatureStats.setAttribute(attribute, std::min( + round_ieee_754(creatureStats.getAttribute(attribute).getBase() + + (level-1) * modifierSum), 100) ); } // initial health @@ -193,18 +215,6 @@ namespace majorMultiplier = 1.0f; break; } - if (class_->mData.mSkills[k][1] == skillIndex) - { - // Major skill -> add starting spells for this skill if existing - const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); - MWWorld::Store::iterator it = store.get().begin(); - for (; it != store.get().end(); ++it) - { - if (it->mData.mFlags & ESM::Spell::F_Autocalc - && MWMechanics::spellSchoolToSkill(MWMechanics::getSpellSchool(&*it, ptr)) == skillIndex) - npcStats.getSpells().add(it->mId); - } - } } // is this skill in the same Specialization as the class? @@ -217,49 +227,69 @@ namespace npcStats.getSkill(skillIndex).setBase( std::min( - npcStats.getSkill(skillIndex).getBase() + round_ieee_754( + npcStats.getSkill(skillIndex).getBase() + 5 + raceBonus + specBonus - + static_cast((level-1) * (majorMultiplier + specMultiplier)), 100)); + +(int(level)-1) * (majorMultiplier + specMultiplier)), 100)); // Must gracefully handle level 0 } + + int skills[ESM::Skill::Length]; + for (int i=0; i spells = MWMechanics::autoCalcNpcSpells(skills, attributes, race); + for (std::vector::iterator it = spells.begin(); it != spells.end(); ++it) + npcStats.getSpells().add(*it); } } namespace MWClass { - void Npc::ensureCustomData (const MWWorld::Ptr& ptr) const + const Npc::GMST& Npc::getGmst() { + static GMST gmst; static bool inited = false; if(!inited) { const MWBase::World *world = MWBase::Environment::get().getWorld(); - const MWWorld::Store &gmst = world->getStore().get(); - - fMinWalkSpeed = gmst.find("fMinWalkSpeed"); - fMaxWalkSpeed = gmst.find("fMaxWalkSpeed"); - fEncumberedMoveEffect = gmst.find("fEncumberedMoveEffect"); - fSneakSpeedMultiplier = gmst.find("fSneakSpeedMultiplier"); - fAthleticsRunBonus = gmst.find("fAthleticsRunBonus"); - fBaseRunMultiplier = gmst.find("fBaseRunMultiplier"); - fMinFlySpeed = gmst.find("fMinFlySpeed"); - fMaxFlySpeed = gmst.find("fMaxFlySpeed"); - fSwimRunBase = gmst.find("fSwimRunBase"); - fSwimRunAthleticsMult = gmst.find("fSwimRunAthleticsMult"); - fJumpEncumbranceBase = gmst.find("fJumpEncumbranceBase"); - fJumpEncumbranceMultiplier = gmst.find("fJumpEncumbranceMultiplier"); - fJumpAcrobaticsBase = gmst.find("fJumpAcrobaticsBase"); - fJumpAcroMultiplier = gmst.find("fJumpAcroMultiplier"); - fJumpRunMultiplier = gmst.find("fJumpRunMultiplier"); - fWereWolfRunMult = gmst.find("fWereWolfRunMult"); - fKnockDownMult = gmst.find("fKnockDownMult"); - iKnockDownOddsMult = gmst.find("iKnockDownOddsMult"); - iKnockDownOddsBase = gmst.find("iKnockDownOddsBase"); - fDamageStrengthBase = gmst.find("fDamageStrengthBase"); - fDamageStrengthMult = gmst.find("fDamageStrengthMult"); + const MWWorld::Store &store = world->getStore().get(); + + gmst.fMinWalkSpeed = store.find("fMinWalkSpeed"); + gmst.fMaxWalkSpeed = store.find("fMaxWalkSpeed"); + gmst.fEncumberedMoveEffect = store.find("fEncumberedMoveEffect"); + gmst.fSneakSpeedMultiplier = store.find("fSneakSpeedMultiplier"); + gmst.fAthleticsRunBonus = store.find("fAthleticsRunBonus"); + gmst.fBaseRunMultiplier = store.find("fBaseRunMultiplier"); + gmst.fMinFlySpeed = store.find("fMinFlySpeed"); + gmst.fMaxFlySpeed = store.find("fMaxFlySpeed"); + gmst.fSwimRunBase = store.find("fSwimRunBase"); + gmst.fSwimRunAthleticsMult = store.find("fSwimRunAthleticsMult"); + gmst.fJumpEncumbranceBase = store.find("fJumpEncumbranceBase"); + gmst.fJumpEncumbranceMultiplier = store.find("fJumpEncumbranceMultiplier"); + gmst.fJumpAcrobaticsBase = store.find("fJumpAcrobaticsBase"); + gmst.fJumpAcroMultiplier = store.find("fJumpAcroMultiplier"); + gmst.fJumpRunMultiplier = store.find("fJumpRunMultiplier"); + gmst.fWereWolfRunMult = store.find("fWereWolfRunMult"); + gmst.fKnockDownMult = store.find("fKnockDownMult"); + gmst.iKnockDownOddsMult = store.find("iKnockDownOddsMult"); + gmst.iKnockDownOddsBase = store.find("iKnockDownOddsBase"); + gmst.fDamageStrengthBase = store.find("fDamageStrengthBase"); + gmst.fDamageStrengthMult = store.find("fDamageStrengthMult"); + gmst.fCombatArmorMinMult = store.find("fCombatArmorMinMult"); inited = true; } + return gmst; + } + + void Npc::ensureCustomData (const MWWorld::Ptr& ptr) const + { if (!ptr.getRefData().getCustomData()) { std::auto_ptr data(new NpcCustomData); @@ -273,11 +303,11 @@ namespace MWClass Misc::StringUtils::toLower(faction); if(ref->mBase->mNpdtType != ESM::NPC::NPC_WITH_AUTOCALCULATED_STATS) { - data->mNpcStats.getFactionRanks()[faction] = (int)ref->mBase->mNpdt52.mRank; + data->mNpcStats.setFactionRank(faction, (int)ref->mBase->mNpdt52.mRank); } else { - data->mNpcStats.getFactionRanks()[faction] = (int)ref->mBase->mNpdt12.mRank; + data->mNpcStats.setFactionRank(faction, (int)ref->mBase->mNpdt12.mRank); } } @@ -306,6 +336,8 @@ namespace MWClass data->mNpcStats.setLevel(ref->mBase->mNpdt52.mLevel); data->mNpcStats.setBaseDisposition(ref->mBase->mNpdt52.mDisposition); data->mNpcStats.setReputation(ref->mBase->mNpdt52.mReputation); + + data->mNpcStats.setNeedRecalcDynamicStats(false); } else { @@ -320,6 +352,8 @@ namespace MWClass autoCalculateAttributes(ref->mBase, data->mNpcStats); autoCalculateSkills(ref->mBase, data->mNpcStats, ptr); + + data->mNpcStats.setNeedRecalcDynamicStats(true); } // race powers @@ -374,9 +408,9 @@ namespace MWClass return ref->mBase->mId; } - void Npc::adjustPosition(const MWWorld::Ptr& ptr) const + void Npc::adjustPosition(const MWWorld::Ptr& ptr, bool force) const { - MWBase::Environment::get().getWorld()->adjustPosition(ptr); + MWBase::Environment::get().getWorld()->adjustPosition(ptr, force); } void Npc::insertObjectRendering (const MWWorld::Ptr& ptr, MWRender::RenderingInterface& renderingInterface) const @@ -404,11 +438,6 @@ namespace MWClass ptr.get(); assert(ref->mBase != NULL); - //std::string headID = ref->mBase->mHead; - - //int end = headID.find_last_of("head_") - 4; - //std::string bodyRaceID = headID.substr(0, end); - std::string model = "meshes\\base_anim.nif"; const ESM::Race* race = MWBase::Environment::get().getWorld()->getStore().get().find(ref->mBase->mRace); if(race->mData.mFlags & ESM::Race::Beast) @@ -423,9 +452,9 @@ namespace MWClass if(getNpcStats(ptr).isWerewolf()) { const MWBase::World *world = MWBase::Environment::get().getWorld(); - const MWWorld::Store &gmst = world->getStore().get(); + const MWWorld::Store &store = world->getStore().get(); - return gmst.find("sWerewolfPopup")->getString(); + return store.find("sWerewolfPopup")->getString(); } MWWorld::LiveCellRef *ref = ptr.get(); @@ -450,7 +479,9 @@ namespace MWClass void Npc::hit(const MWWorld::Ptr& ptr, int type) const { MWBase::World *world = MWBase::Environment::get().getWorld(); - const MWWorld::Store &gmst = world->getStore().get(); + const GMST& gmst = getGmst(); + + const MWWorld::Store &store = world->getStore().get(); // Get the weapon used (if hand-to-hand, weapon = inv.end()) MWWorld::InventoryStore &inv = getInventoryStore(ptr); @@ -461,21 +492,21 @@ namespace MWClass // Reduce fatigue // somewhat of a guess, but using the weapon weight makes sense - const float fFatigueAttackBase = gmst.find("fFatigueAttackBase")->getFloat(); - const float fFatigueAttackMult = gmst.find("fFatigueAttackMult")->getFloat(); - const float fWeaponFatigueMult = gmst.find("fWeaponFatigueMult")->getFloat(); + const float fFatigueAttackBase = store.find("fFatigueAttackBase")->getFloat(); + const float fFatigueAttackMult = store.find("fFatigueAttackMult")->getFloat(); + const float fWeaponFatigueMult = store.find("fWeaponFatigueMult")->getFloat(); MWMechanics::DynamicStat fatigue = getCreatureStats(ptr).getFatigue(); - const float normalizedEncumbrance = getEncumbrance(ptr) / getCapacity(ptr); + const float normalizedEncumbrance = getNormalizedEncumbrance(ptr); float fatigueLoss = fFatigueAttackBase + normalizedEncumbrance * fFatigueAttackMult; if (!weapon.isEmpty()) fatigueLoss += weapon.getClass().getWeight(weapon) * getNpcStats(ptr).getAttackStrength() * fWeaponFatigueMult; fatigue.setCurrent(fatigue.getCurrent() - fatigueLoss); getCreatureStats(ptr).setFatigue(fatigue); - const float fCombatDistance = gmst.find("fCombatDistance")->getFloat(); + const float fCombatDistance = store.find("fCombatDistance")->getFloat(); float dist = fCombatDistance * (!weapon.isEmpty() ? weapon.get()->mBase->mData.mReach : - gmst.find("fHandToHandReach")->getFloat()); + store.find("fHandToHandReach")->getFloat()); // TODO: Use second to work out the hit angle std::pair result = world->getHitContact(ptr, dist); @@ -503,6 +534,7 @@ namespace MWClass if((::rand()/(RAND_MAX+1.0)) > hitchance/100.0f) { othercls.onHit(victim, 0.0f, false, weapon, ptr, false); + MWMechanics::reduceWeaponCondition(0.f, false, weapon, ptr); return; } @@ -511,7 +543,6 @@ namespace MWClass MWMechanics::NpcStats &stats = getNpcStats(ptr); if(!weapon.isEmpty()) { - const bool weaphashealth = weapon.getClass().hasItemHealth(weapon); const unsigned char *attack = NULL; if(type == ESM::Weapon::AT_Chop) attack = weapon.get()->mBase->mData.mChop; @@ -522,29 +553,11 @@ namespace MWClass if(attack) { damage = attack[0] + ((attack[1]-attack[0])*stats.getAttackStrength()); - damage *= fDamageStrengthBase->getFloat() + - (stats.getAttribute(ESM::Attribute::Strength).getModified() * fDamageStrengthMult->getFloat() * 0.1); - if(weaphashealth) - { - int weapmaxhealth = weapon.getClass().getItemMaxHealth(weapon); - int weaphealth = weapon.getClass().getItemHealth(weapon); - - damage *= float(weaphealth) / weapmaxhealth; - - if (!MWBase::Environment::get().getWorld()->getGodModeState()) - { - // Reduce weapon charge by at least one, but cap at 0 - weaphealth -= std::min(std::max(1, - (int)(damage * gmst.find("fWeaponDamageMult")->getFloat())), weaphealth); - - weapon.getCellRef().setCharge(weaphealth); - } - - // Weapon broken? unequip it - if (weaphealth == 0) - weapon = *inv.unequipItem(weapon, ptr); - } + damage *= gmst.fDamageStrengthBase->getFloat() + + (stats.getAttribute(ESM::Attribute::Strength).getModified() * gmst.fDamageStrengthMult->getFloat() * 0.1); } + MWMechanics::adjustWeaponDamage(damage, weapon); + MWMechanics::reduceWeaponCondition(damage, true, weapon, ptr); healthdmg = true; } else @@ -552,22 +565,21 @@ namespace MWClass // Note: MCP contains an option to include Strength in hand-to-hand damage // calculations. Some mods recommend using it, so we may want to include am // option for it. - float minstrike = gmst.find("fMinHandToHandMult")->getFloat(); - float maxstrike = gmst.find("fMaxHandToHandMult")->getFloat(); + float minstrike = store.find("fMinHandToHandMult")->getFloat(); + float maxstrike = store.find("fMaxHandToHandMult")->getFloat(); damage = stats.getSkill(weapskill).getModified(); damage *= minstrike + ((maxstrike-minstrike)*stats.getAttackStrength()); - healthdmg = (otherstats.getFatigue().getCurrent() < 1.0f) - || (otherstats.getMagicEffects().get(ESM::MagicEffect::Paralyze).mMagnitude > 0); + healthdmg = (otherstats.getMagicEffects().get(ESM::MagicEffect::Paralyze).getMagnitude() > 0) + || otherstats.getKnockedDown(); if(stats.isWerewolf()) { healthdmg = true; // GLOB instead of GMST because it gets updated during a quest - const MWWorld::Store &glob = world->getStore().get(); - damage *= glob.find("WerewolfClawMult")->mValue.getFloat(); + damage *= world->getGlobalFloat("werewolfclawmult"); } if(healthdmg) - damage *= gmst.find("fHandtoHandHealthPer")->getFloat(); + damage *= store.find("fHandtoHandHealthPer")->getFloat(); MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); if(stats.isWerewolf()) @@ -580,17 +592,23 @@ namespace MWClass sndMgr->playSound3D(victim, "Hand To Hand Hit", 1.0f, 1.0f); } if(ptr.getRefData().getHandle() == "player") + { skillUsageSucceeded(ptr, weapskill, 0); - bool detected = MWBase::Environment::get().getMechanicsManager()->awarenessCheck(ptr, victim); - if(!detected) - { - damage *= gmst.find("fCombatCriticalStrikeMult")->getFloat(); - MWBase::Environment::get().getWindowManager()->messageBox("#{sTargetCriticalStrike}"); - MWBase::Environment::get().getSoundManager()->playSound3D(victim, "critical damage", 1.0f, 1.0f); + const MWMechanics::AiSequence& seq = victim.getClass().getCreatureStats(victim).getAiSequence(); + + bool unaware = !seq.isInCombat() + && !MWBase::Environment::get().getMechanicsManager()->awarenessCheck(ptr, victim); + if(unaware) + { + damage *= store.find("fCombatCriticalStrikeMult")->getFloat(); + MWBase::Environment::get().getWindowManager()->messageBox("#{sTargetCriticalStrike}"); + MWBase::Environment::get().getSoundManager()->playSound3D(victim, "critical damage", 1.0f, 1.0f); + } } + if (othercls.getCreatureStats(victim).getKnockedDown()) - damage *= gmst.find("fCombatKODamageMult")->getFloat(); + damage *= store.find("fCombatKODamageMult")->getFloat(); // Apply "On hit" enchanted weapons std::string enchantmentName = !weapon.isEmpty() ? weapon.getClass().getEnchantment(weapon) : ""; @@ -606,6 +624,8 @@ namespace MWClass } } + MWMechanics::applyElementalShields(ptr, victim); + if (!weapon.isEmpty() && MWMechanics::blockMeleeAttack(ptr, victim, weapon, damage)) damage = 0; @@ -623,13 +643,16 @@ namespace MWClass // NOTE: 'object' and/or 'attacker' may be empty. - // Attacking peaceful NPCs is a crime - // anything below 80 is considered peaceful (see Actors::updateActor) - if (!attacker.isEmpty() && !ptr.getClass().getCreatureStats(ptr).isHostile() && - ptr.getClass().getCreatureStats(ptr).getAiSetting(MWMechanics::CreatureStats::AI_Fight).getModified() < 80) - MWBase::Environment::get().getMechanicsManager()->commitCrime(attacker, ptr, MWBase::MechanicsManager::OT_Assault); + bool wasDead = getCreatureStats(ptr).isDead(); - getCreatureStats(ptr).setAttacked(true); + // Note OnPcHitMe is not set for friendly hits. + bool setOnPcHitMe = true; + if (!attacker.isEmpty() && !ptr.getClass().getCreatureStats(ptr).getAiSequence().isInCombat(attacker)) + { + getCreatureStats(ptr).setAttacked(true); + + setOnPcHitMe = MWBase::Environment::get().getMechanicsManager()->actorAttacked(ptr, attacker); + } if(!successful) { @@ -643,7 +666,7 @@ namespace MWClass if(!object.isEmpty()) getCreatureStats(ptr).setLastHitObject(object.getClass().getId(object)); - if(!attacker.isEmpty() && attacker.getRefData().getHandle() == "player") + if(setOnPcHitMe && !attacker.isEmpty() && attacker.getRefData().getHandle() == "player") { const std::string &script = ptr.getClass().getScript(ptr); /* Set the OnPCHitMe script variable. The script is responsible for clearing it. */ @@ -654,12 +677,17 @@ namespace MWClass if (damage > 0.0f && !object.isEmpty()) MWMechanics::resistNormalWeapon(ptr, attacker, object, damage); - if(damage > 0.0f) + if (damage < 0.001f) + damage = 0; + + if(damage > 0.0f && !attacker.isEmpty()) { // 'ptr' is losing health. Play a 'hit' voiced dialog entry if not already saying // something, alert the character controller, scripts, etc. const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); + const GMST& gmst = getGmst(); + int chance = store.get().find("iVoiceHitOdds")->getInt(); int roll = std::rand()/ (static_cast (RAND_MAX) + 1) * 100; // [0, 99] if (roll < chance) @@ -668,9 +696,9 @@ namespace MWClass } // Check for knockdown - float agilityTerm = getCreatureStats(ptr).getAttribute(ESM::Attribute::Agility).getModified() * fKnockDownMult->getFloat(); + float agilityTerm = getCreatureStats(ptr).getAttribute(ESM::Attribute::Agility).getModified() * gmst.fKnockDownMult->getFloat(); float knockdownTerm = getCreatureStats(ptr).getAttribute(ESM::Attribute::Agility).getModified() - * iKnockDownOddsMult->getInt() * 0.01 + iKnockDownOddsBase->getInt(); + * gmst.iKnockDownOddsMult->getInt() * 0.01 + gmst.iKnockDownOddsBase->getInt(); roll = std::rand()/ (static_cast (RAND_MAX) + 1) * 100; // [0, 99] if (ishealth && agilityTerm <= damage && knockdownTerm <= roll) { @@ -680,7 +708,7 @@ namespace MWClass else getCreatureStats(ptr).setHitRecovery(true); // Is this supposed to always occur? - if(ishealth) + if(damage > 0 && ishealth) { // Hit percentages: // cuirass = 30% @@ -700,9 +728,12 @@ namespace MWClass }; int hitslot = hitslots[(int)(::rand()/(RAND_MAX+1.0)*20.0)]; - float damagediff = damage; - damage /= std::min(1.0f + getArmorRating(ptr)/std::max(1.0f, damage), 4.0f); - damagediff -= damage; + float unmitigatedDamage = damage; + float x = damage / (damage + getArmorRating(ptr)); + damage *= std::max(gmst.fCombatArmorMinMult->getFloat(), x); + int damageDiff = unmitigatedDamage - damage; + if (damage < 1) + damage = 1; MWWorld::InventoryStore &inv = getInventoryStore(ptr); MWWorld::ContainerStoreIterator armorslot = inv.getSlot(hitslot); @@ -710,13 +741,13 @@ namespace MWClass if(!armor.isEmpty() && armor.getTypeName() == typeid(ESM::Armor).name()) { int armorhealth = armor.getClass().getItemHealth(armor); - armorhealth -= std::min(std::max(1, (int)damagediff), + armorhealth -= std::min(std::max(1, damageDiff), armorhealth); armor.getCellRef().setCharge(armorhealth); // Armor broken? unequip it if (armorhealth == 0) - inv.unequipItem(armor, ptr); + armor = *inv.unequipItem(armor, ptr); if (ptr.getRefData().getHandle() == "player") skillUsageSucceeded(ptr, armor.getClass().getEquipmentSkill(armor), 0); @@ -741,8 +772,15 @@ namespace MWClass if(ishealth) { + if (!attacker.isEmpty()) + damage = scaleDamage(damage, attacker, ptr); + if(damage > 0.0f) + { sndMgr->playSound3D(ptr, "Health Damage", 1.0f, 1.0f); + if (ptr.getRefData().getHandle() == "player") + MWBase::Environment::get().getWindowManager()->activateHitOverlay(); + } float health = getCreatureStats(ptr).getHealth().getCurrent() - damage; setActorHealth(ptr, health, attacker); } @@ -752,6 +790,22 @@ namespace MWClass fatigue.setCurrent(fatigue.getCurrent() - damage, true); getCreatureStats(ptr).setFatigue(fatigue); } + + if (!wasDead && getCreatureStats(ptr).isDead()) + { + // NPC was killed + if (!attacker.isEmpty() && attacker.getClass().isNpc() && attacker.getClass().getNpcStats(attacker).isWerewolf()) + { + attacker.getClass().getNpcStats(attacker).addWerewolfKill(); + } + + // Simple check for who attacked first: if the player attacked first, a crimeId should be set + // Doesn't handle possible edge case where no one reported the assault, but in such a case, + // for bystanders it is not possible to tell who attacked first, anyway. + MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); + if (attacker == player && ptr.getClass().getNpcStats(ptr).getCrimeId() != -1 && ptr != player) + MWBase::Environment::get().getMechanicsManager()->commitCrime(player, ptr, MWBase::MechanicsManager::OT_Murder); + } } void Npc::block(const MWWorld::Ptr &ptr) const @@ -805,6 +859,7 @@ namespace MWClass if(ptr.getRefData().getHandle() == "player") return boost::shared_ptr(new MWWorld::ActionTalk(actor)); + // Werewolfs can't activate NPCs if(actor.getClass().isNpc() && actor.getClass().getNpcStats(actor).isWerewolf()) { const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); @@ -815,15 +870,18 @@ namespace MWClass return action; } + if(getCreatureStats(ptr).isDead()) return boost::shared_ptr(new MWWorld::ActionOpen(ptr, true)); - if(ptr.getClass().getCreatureStats(ptr).isHostile()) + if(ptr.getClass().getCreatureStats(ptr).getAiSequence().isInCombat()) return boost::shared_ptr(new MWWorld::FailedAction("#{sActorInCombat}")); - if(getCreatureStats(actor).getStance(MWMechanics::CreatureStats::Stance_Sneak)) + if(getCreatureStats(actor).getStance(MWMechanics::CreatureStats::Stance_Sneak) + || ptr.getClass().getCreatureStats(ptr).getKnockedDown()) return boost::shared_ptr(new MWWorld::ActionOpen(ptr)); // stealing - + // Can't talk to werewolfs + if(ptr.getClass().isNpc() && ptr.getClass().getNpcStats(ptr).isWerewolf()) + return boost::shared_ptr (new MWWorld::FailedAction("")); return boost::shared_ptr(new MWWorld::ActionTalk(ptr)); - } MWWorld::ContainerStore& Npc::getContainerStore (const MWWorld::Ptr& ptr) @@ -853,36 +911,36 @@ namespace MWClass float Npc::getSpeed(const MWWorld::Ptr& ptr) const { const MWBase::World *world = MWBase::Environment::get().getWorld(); + const GMST& gmst = getGmst(); + const NpcCustomData *npcdata = static_cast(ptr.getRefData().getCustomData()); const MWMechanics::MagicEffects &mageffects = npcdata->mNpcStats.getMagicEffects(); - const float normalizedEncumbrance = Npc::getEncumbrance(ptr) / Npc::getCapacity(ptr); + const float normalizedEncumbrance = getNormalizedEncumbrance(ptr); bool sneaking = ptr.getClass().getCreatureStats(ptr).getStance(MWMechanics::CreatureStats::Stance_Sneak); bool running = ptr.getClass().getCreatureStats(ptr).getStance(MWMechanics::CreatureStats::Stance_Run); - float walkSpeed = fMinWalkSpeed->getFloat() + 0.01f*npcdata->mNpcStats.getAttribute(ESM::Attribute::Speed).getModified()* - (fMaxWalkSpeed->getFloat() - fMinWalkSpeed->getFloat()); - walkSpeed *= 1.0f - fEncumberedMoveEffect->getFloat()*normalizedEncumbrance; + float walkSpeed = gmst.fMinWalkSpeed->getFloat() + 0.01f*npcdata->mNpcStats.getAttribute(ESM::Attribute::Speed).getModified()* + (gmst.fMaxWalkSpeed->getFloat() - gmst.fMinWalkSpeed->getFloat()); + walkSpeed *= 1.0f - gmst.fEncumberedMoveEffect->getFloat()*normalizedEncumbrance; walkSpeed = std::max(0.0f, walkSpeed); if(sneaking) - walkSpeed *= fSneakSpeedMultiplier->getFloat(); + walkSpeed *= gmst.fSneakSpeedMultiplier->getFloat(); float runSpeed = walkSpeed*(0.01f * npcdata->mNpcStats.getSkill(ESM::Skill::Athletics).getModified() * - fAthleticsRunBonus->getFloat() + fBaseRunMultiplier->getFloat()); - if(npcdata->mNpcStats.isWerewolf()) - runSpeed *= fWereWolfRunMult->getFloat(); + gmst.fAthleticsRunBonus->getFloat() + gmst.fBaseRunMultiplier->getFloat()); float moveSpeed; - if(normalizedEncumbrance >= 1.0f) + if(getEncumbrance(ptr) > getCapacity(ptr)) moveSpeed = 0.0f; - else if(mageffects.get(ESM::MagicEffect::Levitate).mMagnitude > 0 && + else if(mageffects.get(ESM::MagicEffect::Levitate).getMagnitude() > 0 && world->isLevitationEnabled()) { float flySpeed = 0.01f*(npcdata->mNpcStats.getAttribute(ESM::Attribute::Speed).getModified() + - mageffects.get(ESM::MagicEffect::Levitate).mMagnitude); - flySpeed = fMinFlySpeed->getFloat() + flySpeed*(fMaxFlySpeed->getFloat() - fMinFlySpeed->getFloat()); - flySpeed *= 1.0f - fEncumberedMoveEffect->getFloat() * normalizedEncumbrance; + mageffects.get(ESM::MagicEffect::Levitate).getMagnitude()); + flySpeed = gmst.fMinFlySpeed->getFloat() + flySpeed*(gmst.fMaxFlySpeed->getFloat() - gmst.fMinFlySpeed->getFloat()); + flySpeed *= 1.0f - gmst.fEncumberedMoveEffect->getFloat() * normalizedEncumbrance; flySpeed = std::max(0.0f, flySpeed); moveSpeed = flySpeed; } @@ -891,9 +949,9 @@ namespace MWClass float swimSpeed = walkSpeed; if(running) swimSpeed = runSpeed; - swimSpeed *= 1.0f + 0.01f * mageffects.get(ESM::MagicEffect::SwiftSwim).mMagnitude; - swimSpeed *= fSwimRunBase->getFloat() + 0.01f*npcdata->mNpcStats.getSkill(ESM::Skill::Athletics).getModified()* - fSwimRunAthleticsMult->getFloat(); + swimSpeed *= 1.0f + 0.01f * mageffects.get(ESM::MagicEffect::SwiftSwim).getMagnitude(); + swimSpeed *= gmst.fSwimRunBase->getFloat() + 0.01f*npcdata->mNpcStats.getSkill(ESM::Skill::Athletics).getModified()* + gmst.fSwimRunAthleticsMult->getFloat(); moveSpeed = swimSpeed; } else if(running && !sneaking) @@ -903,15 +961,19 @@ namespace MWClass if(getMovementSettings(ptr).mPosition[0] != 0 && getMovementSettings(ptr).mPosition[1] == 0) moveSpeed *= 0.75f; + if(npcdata->mNpcStats.isWerewolf() && running && npcdata->mNpcStats.getDrawState() == MWMechanics::DrawState_Nothing) + moveSpeed *= gmst.fWereWolfRunMult->getFloat(); + return moveSpeed; } float Npc::getJump(const MWWorld::Ptr &ptr) const { const NpcCustomData *npcdata = static_cast(ptr.getRefData().getCustomData()); + const GMST& gmst = getGmst(); const MWMechanics::MagicEffects &mageffects = npcdata->mNpcStats.getMagicEffects(); - const float encumbranceTerm = fJumpEncumbranceBase->getFloat() + - fJumpEncumbranceMultiplier->getFloat() * + const float encumbranceTerm = gmst.fJumpEncumbranceBase->getFloat() + + gmst.fJumpEncumbranceMultiplier->getFloat() * (1.0f - Npc::getEncumbrance(ptr)/Npc::getCapacity(ptr)); float a = npcdata->mNpcStats.getSkill(ESM::Skill::Acrobatics).getModified(); @@ -922,14 +984,14 @@ namespace MWClass a = 50.0f; } - float x = fJumpAcrobaticsBase->getFloat() + - std::pow(a / 15.0f, fJumpAcroMultiplier->getFloat()); - x += 3.0f * b * fJumpAcroMultiplier->getFloat(); - x += mageffects.get(ESM::MagicEffect::Jump).mMagnitude * 64; + float x = gmst.fJumpAcrobaticsBase->getFloat() + + std::pow(a / 15.0f, gmst.fJumpAcroMultiplier->getFloat()); + x += 3.0f * b * gmst.fJumpAcroMultiplier->getFloat(); + x += mageffects.get(ESM::MagicEffect::Jump).getMagnitude() * 64; x *= encumbranceTerm; if(ptr.getClass().getCreatureStats(ptr).getStance(MWMechanics::CreatureStats::Stance_Run)) - x *= fJumpRunMultiplier->getFloat(); + x *= gmst.fJumpRunMultiplier->getFloat(); x *= npcdata->mNpcStats.getFatigueTerm(); x -= -627.2f;/*gravity constant*/ x /= 3.0f; @@ -940,19 +1002,19 @@ namespace MWClass float Npc::getFallDamage(const MWWorld::Ptr &ptr, float fallHeight) const { MWBase::World *world = MWBase::Environment::get().getWorld(); - const MWWorld::Store &gmst = world->getStore().get(); + const MWWorld::Store &store = world->getStore().get(); - const float fallDistanceMin = gmst.find("fFallDamageDistanceMin")->getFloat(); + const float fallDistanceMin = store.find("fFallDamageDistanceMin")->getFloat(); if (fallHeight >= fallDistanceMin) { const float acrobaticsSkill = ptr.getClass().getNpcStats (ptr).getSkill(ESM::Skill::Acrobatics).getModified(); const NpcCustomData *npcdata = static_cast(ptr.getRefData().getCustomData()); - const float jumpSpellBonus = npcdata->mNpcStats.getMagicEffects().get(ESM::MagicEffect::Jump).mMagnitude; - const float fallAcroBase = gmst.find("fFallAcroBase")->getFloat(); - const float fallAcroMult = gmst.find("fFallAcroMult")->getFloat(); - const float fallDistanceBase = gmst.find("fFallDistanceBase")->getFloat(); - const float fallDistanceMult = gmst.find("fFallDistanceMult")->getFloat(); + const float jumpSpellBonus = npcdata->mNpcStats.getMagicEffects().get(ESM::MagicEffect::Jump).getMagnitude(); + const float fallAcroBase = store.find("fFallAcroBase")->getFloat(); + const float fallAcroMult = store.find("fFallAcroMult")->getFloat(); + const float fallDistanceBase = store.find("fFallDistanceBase")->getFloat(); + const float fallDistanceMult = store.find("fFallDistanceMult")->getFloat(); float x = fallHeight - fallDistanceMin; x -= (1.5 * acrobaticsSkill) + jumpSpellBonus; @@ -1011,9 +1073,7 @@ namespace MWClass bool Npc::hasToolTip (const MWWorld::Ptr& ptr) const { - /// \todo We don't want tooltips for NPCs in combat mode. - - return true; + return !ptr.getClass().getCreatureStats(ptr).getAiSequence().isInCombat() || getCreatureStats(ptr).isDead(); } MWGui::ToolTipInfo Npc::getToolTipInfo (const MWWorld::Ptr& ptr) const @@ -1054,8 +1114,8 @@ namespace MWClass if(!stats.isWerewolf()) { weight = getContainerStore(ptr).getWeight(); - weight -= stats.getMagicEffects().get(ESM::MagicEffect::Feather).mMagnitude; - weight += stats.getMagicEffects().get(ESM::MagicEffect::Burden).mMagnitude; + weight -= stats.getMagicEffects().get(ESM::MagicEffect::Feather).getMagnitude(); + weight += stats.getMagicEffects().get(ESM::MagicEffect::Burden).getMagnitude(); if(weight < 0.0f) weight = 0.0f; } @@ -1070,10 +1130,13 @@ namespace MWClass return cast.cast(id); } - void Npc::skillUsageSucceeded (const MWWorld::Ptr& ptr, int skill, int usageType) const + void Npc::skillUsageSucceeded (const MWWorld::Ptr& ptr, int skill, int usageType, float extraFactor) const { MWMechanics::NpcStats& stats = getNpcStats (ptr); + if (stats.isWerewolf()) + return; + MWWorld::LiveCellRef *ref = ptr.get(); const ESM::Class *class_ = @@ -1081,20 +1144,20 @@ namespace MWClass ref->mBase->mClass ); - stats.useSkill (skill, *class_, usageType); + stats.useSkill (skill, *class_, usageType, extraFactor); } float Npc::getArmorRating (const MWWorld::Ptr& ptr) const { const MWBase::World *world = MWBase::Environment::get().getWorld(); - const MWWorld::Store &gmst = world->getStore().get(); + const MWWorld::Store &store = world->getStore().get(); MWMechanics::NpcStats &stats = getNpcStats(ptr); MWWorld::InventoryStore &invStore = getInventoryStore(ptr); - int iBaseArmorSkill = gmst.find("iBaseArmorSkill")->getInt(); - float fUnarmoredBase1 = gmst.find("fUnarmoredBase1")->getFloat(); - float fUnarmoredBase2 = gmst.find("fUnarmoredBase2")->getFloat(); + int iBaseArmorSkill = store.find("iBaseArmorSkill")->getInt(); + float fUnarmoredBase1 = store.find("fUnarmoredBase1")->getFloat(); + float fUnarmoredBase2 = store.find("fUnarmoredBase2")->getFloat(); int unarmoredSkill = stats.getSkill(ESM::Skill::Unarmored).getModified(); int ratings[MWWorld::InventoryStore::Slots]; @@ -1120,7 +1183,7 @@ namespace MWClass } } - float shield = stats.getMagicEffects().get(ESM::MagicEffect::Shield).mMagnitude; + float shield = stats.getMagicEffects().get(ESM::MagicEffect::Shield).getMagnitude(); return ratings[MWWorld::InventoryStore::Slot_Cuirass] * 0.3f + (ratings[MWWorld::InventoryStore::Slot_CarriedLeft] + ratings[MWWorld::InventoryStore::Slot_Helmet] @@ -1132,13 +1195,6 @@ namespace MWClass + shield; } - - void Npc::adjustRotation(const MWWorld::Ptr& ptr,float& x,float& y,float& z) const - { - y = 0; - x = 0; - } - void Npc::adjustScale(const MWWorld::Ptr &ptr, float &scale) const { MWWorld::LiveCellRef *ref = @@ -1165,65 +1221,48 @@ namespace MWClass std::string Npc::getSoundIdFromSndGen(const MWWorld::Ptr &ptr, const std::string &name) const { - if(name == "left") + if(name == "left" || name == "right") { MWBase::World *world = MWBase::Environment::get().getWorld(); Ogre::Vector3 pos(ptr.getRefData().getPosition().pos); if(world->isSwimming(ptr)) - return "Swim Left"; - if(world->isUnderwater(ptr.getCell(), pos)) - return "FootWaterLeft"; + return (name == "left") ? "Swim Left" : "Swim Right"; + if(world->isUnderwater(ptr.getCell(), pos) || world->isWalkingOnWater(ptr)) + return (name == "left") ? "FootWaterLeft" : "FootWaterRight"; if(world->isOnGround(ptr)) { - MWWorld::InventoryStore &inv = Npc::getInventoryStore(ptr); - MWWorld::ContainerStoreIterator boots = inv.getSlot(MWWorld::InventoryStore::Slot_Boots); - if(boots == inv.end() || boots->getTypeName() != typeid(ESM::Armor).name()) - return "FootBareLeft"; - - switch(boots->getClass().getEquipmentSkill(*boots)) + if (ptr.getClass().getNpcStats(ptr).isWerewolf() + && ptr.getClass().getCreatureStats(ptr).getStance(MWMechanics::CreatureStats::Stance_Run)) { - case ESM::Skill::LightArmor: - return "FootLightLeft"; - case ESM::Skill::MediumArmor: - return "FootMedLeft"; - case ESM::Skill::HeavyArmor: - return "FootHeavyLeft"; + MWMechanics::WeaponType weaponType = MWMechanics::WeapType_None; + MWMechanics::getActiveWeapon(ptr.getClass().getCreatureStats(ptr), ptr.getClass().getInventoryStore(ptr), &weaponType); + if (weaponType == MWMechanics::WeapType_None) + return ""; } - } - return ""; - } - if(name == "right") - { - MWBase::World *world = MWBase::Environment::get().getWorld(); - Ogre::Vector3 pos(ptr.getRefData().getPosition().pos); - if(world->isSwimming(ptr)) - return "Swim Right"; - if(world->isUnderwater(ptr.getCell(), pos)) - return "FootWaterRight"; - if(world->isOnGround(ptr)) - { + MWWorld::InventoryStore &inv = Npc::getInventoryStore(ptr); MWWorld::ContainerStoreIterator boots = inv.getSlot(MWWorld::InventoryStore::Slot_Boots); if(boots == inv.end() || boots->getTypeName() != typeid(ESM::Armor).name()) - return "FootBareRight"; + return (name == "left") ? "FootBareLeft" : "FootBareRight"; switch(boots->getClass().getEquipmentSkill(*boots)) { case ESM::Skill::LightArmor: - return "FootLightRight"; + return (name == "left") ? "FootLightLeft" : "FootLightRight"; case ESM::Skill::MediumArmor: - return "FootMedRight"; + return (name == "left") ? "FootMedLeft" : "FootMedRight"; case ESM::Skill::HeavyArmor: - return "FootHeavyRight"; + return (name == "left") ? "FootHeavyLeft" : "FootHeavyRight"; } } return ""; } + if(name == "land") { MWBase::World *world = MWBase::Environment::get().getWorld(); Ogre::Vector3 pos(ptr.getRefData().getPosition().pos); - if(world->isUnderwater(ptr.getCell(), pos)) + if(world->isUnderwater(ptr.getCell(), pos) || world->isWalkingOnWater(ptr)) return "DefaultLandWater"; if(world->isOnGround(ptr)) return "Body Fall Medium"; @@ -1276,7 +1315,18 @@ namespace MWClass { const ESM::NpcState& state2 = dynamic_cast (state); - ensureCustomData (ptr); + ensureCustomData(ptr); + // If we do the following instead we get a sizable speedup, but this causes compatibility issues + // with 0.30 savegames, where some state in CreatureStats was not saved yet, + // and therefore needs to be loaded from ESM records. TODO: re-enable this in a future release. + /* + if (!ptr.getRefData().getCustomData()) + { + // Create a CustomData, but don't fill it from ESM records (not needed) + std::auto_ptr data (new NpcCustomData); + ptr.getRefData().setCustomData (data.release()); + } + */ NpcCustomData& customData = dynamic_cast (*ptr.getRefData().getCustomData()); @@ -1325,8 +1375,7 @@ namespace MWClass ptr.getRefData().setCount(1); // Reset to original position - ESM::Position& pos = ptr.getRefData().getPosition(); - pos = ptr.getCellRef().getPosition(); + ptr.getRefData().setPosition(ptr.getCellRef().getPosition()); ptr.getRefData().setCustomData(NULL); } @@ -1340,27 +1389,4 @@ namespace MWClass MWWorld::ContainerStore& store = getContainerStore(ptr); store.restock(list, ptr, ptr.getCellRef().getRefId(), ptr.getCellRef().getFaction()); } - - const ESM::GameSetting *Npc::fMinWalkSpeed; - const ESM::GameSetting *Npc::fMaxWalkSpeed; - const ESM::GameSetting *Npc::fEncumberedMoveEffect; - const ESM::GameSetting *Npc::fSneakSpeedMultiplier; - const ESM::GameSetting *Npc::fAthleticsRunBonus; - const ESM::GameSetting *Npc::fBaseRunMultiplier; - const ESM::GameSetting *Npc::fMinFlySpeed; - const ESM::GameSetting *Npc::fMaxFlySpeed; - const ESM::GameSetting *Npc::fSwimRunBase; - const ESM::GameSetting *Npc::fSwimRunAthleticsMult; - const ESM::GameSetting *Npc::fJumpEncumbranceBase; - const ESM::GameSetting *Npc::fJumpEncumbranceMultiplier; - const ESM::GameSetting *Npc::fJumpAcrobaticsBase; - const ESM::GameSetting *Npc::fJumpAcroMultiplier; - const ESM::GameSetting *Npc::fJumpRunMultiplier; - const ESM::GameSetting *Npc::fWereWolfRunMult; - const ESM::GameSetting *Npc::fKnockDownMult; - const ESM::GameSetting *Npc::iKnockDownOddsMult; - const ESM::GameSetting *Npc::iKnockDownOddsBase; - const ESM::GameSetting *Npc::fDamageStrengthBase; - const ESM::GameSetting *Npc::fDamageStrengthMult; - } diff --git a/apps/openmw/mwclass/npc.hpp b/apps/openmw/mwclass/npc.hpp index 356e358b9..fd16e6f08 100644 --- a/apps/openmw/mwclass/npc.hpp +++ b/apps/openmw/mwclass/npc.hpp @@ -17,27 +17,33 @@ namespace MWClass virtual MWWorld::Ptr copyToCellImpl(const MWWorld::Ptr &ptr, MWWorld::CellStore &cell) const; - static const ESM::GameSetting *fMinWalkSpeed; - static const ESM::GameSetting *fMaxWalkSpeed; - static const ESM::GameSetting *fEncumberedMoveEffect; - static const ESM::GameSetting *fSneakSpeedMultiplier; - static const ESM::GameSetting *fAthleticsRunBonus; - static const ESM::GameSetting *fBaseRunMultiplier; - static const ESM::GameSetting *fMinFlySpeed; - static const ESM::GameSetting *fMaxFlySpeed; - static const ESM::GameSetting *fSwimRunBase; - static const ESM::GameSetting *fSwimRunAthleticsMult; - static const ESM::GameSetting *fJumpEncumbranceBase; - static const ESM::GameSetting *fJumpEncumbranceMultiplier; - static const ESM::GameSetting *fJumpAcrobaticsBase; - static const ESM::GameSetting *fJumpAcroMultiplier; - static const ESM::GameSetting *fJumpRunMultiplier; - static const ESM::GameSetting *fWereWolfRunMult; - static const ESM::GameSetting *fKnockDownMult; - static const ESM::GameSetting *iKnockDownOddsMult; - static const ESM::GameSetting *iKnockDownOddsBase; - static const ESM::GameSetting *fDamageStrengthBase; - static const ESM::GameSetting *fDamageStrengthMult; + struct GMST + { + const ESM::GameSetting *fMinWalkSpeed; + const ESM::GameSetting *fMaxWalkSpeed; + const ESM::GameSetting *fEncumberedMoveEffect; + const ESM::GameSetting *fSneakSpeedMultiplier; + const ESM::GameSetting *fAthleticsRunBonus; + const ESM::GameSetting *fBaseRunMultiplier; + const ESM::GameSetting *fMinFlySpeed; + const ESM::GameSetting *fMaxFlySpeed; + const ESM::GameSetting *fSwimRunBase; + const ESM::GameSetting *fSwimRunAthleticsMult; + const ESM::GameSetting *fJumpEncumbranceBase; + const ESM::GameSetting *fJumpEncumbranceMultiplier; + const ESM::GameSetting *fJumpAcrobaticsBase; + const ESM::GameSetting *fJumpAcroMultiplier; + const ESM::GameSetting *fJumpRunMultiplier; + const ESM::GameSetting *fWereWolfRunMult; + const ESM::GameSetting *fKnockDownMult; + const ESM::GameSetting *iKnockDownOddsMult; + const ESM::GameSetting *iKnockDownOddsBase; + const ESM::GameSetting *fDamageStrengthBase; + const ESM::GameSetting *fDamageStrengthMult; + const ESM::GameSetting *fCombatArmorMinMult; + }; + + static const GMST& getGmst(); public: @@ -49,7 +55,9 @@ namespace MWClass virtual void insertObject(const MWWorld::Ptr& ptr, MWWorld::PhysicsSystem& physics) const; - virtual void adjustPosition(const MWWorld::Ptr& ptr) const; + virtual void adjustPosition(const MWWorld::Ptr& ptr, bool force) const; + ///< Adjust position to stand on ground. Must be called post model load + /// @param force do this even if the ptr is flying virtual std::string getName (const MWWorld::Ptr& ptr) const; ///< \return name (the one that is to be presented to the user; not the internal one); @@ -128,11 +136,9 @@ namespace MWClass virtual void adjustScale (const MWWorld::Ptr &ptr, float &scale) const; - virtual void skillUsageSucceeded (const MWWorld::Ptr& ptr, int skill, int usageType) const; + virtual void skillUsageSucceeded (const MWWorld::Ptr& ptr, int skill, int usageType, float extraFactor=1.f) const; ///< Inform actor \a ptr that a skill use has succeeded. - virtual void adjustRotation(const MWWorld::Ptr& ptr,float& x,float& y,float& z) const; - virtual bool isEssential (const MWWorld::Ptr& ptr) const; ///< Is \a ptr essential? (i.e. may losing \a ptr make the game unwinnable) diff --git a/apps/openmw/mwclass/potion.cpp b/apps/openmw/mwclass/potion.cpp index 7440617c2..2da213c8d 100644 --- a/apps/openmw/mwclass/potion.cpp +++ b/apps/openmw/mwclass/potion.cpp @@ -24,6 +24,11 @@ namespace MWClass { + std::string Potion::getId (const MWWorld::Ptr& ptr) const + { + return ptr.get()->mBase->mId; + } + void Potion::insertObjectRendering (const MWWorld::Ptr& ptr, MWRender::RenderingInterface& renderingInterface) const { const std::string model = getModel(ptr); @@ -127,7 +132,7 @@ namespace MWClass std::string text; text += "\n#{sWeight}: " + MWGui::ToolTips::toString(ref->mBase->mData.mWeight); - text += MWGui::ToolTips::getValueString(getValue(ptr), "#{sValue}"); + text += MWGui::ToolTips::getValueString(ref->mBase->mData.mValue, "#{sValue}"); info.effects = MWGui::Widgets::MWEffectList::effectListFromESM(&ref->mBase->mEffects); @@ -140,19 +145,15 @@ namespace MWClass MWBase::Environment::get().getWorld()->getStore().get().find("fWortChanceValue")->getFloat(); for (MWGui::Widgets::SpellEffectList::iterator it = info.effects.begin(); it != info.effects.end(); ++it) { - it->mKnown = ( (i == 0 && alchemySkill >= fWortChanceValue) - || (i == 1 && alchemySkill >= fWortChanceValue*2) - || (i == 2 && alchemySkill >= fWortChanceValue*3) - || (i == 3 && alchemySkill >= fWortChanceValue*4)); - + it->mKnown = (i <= 1 && alchemySkill >= fWortChanceValue) + || (i <= 3 && alchemySkill >= fWortChanceValue*2); ++i; } info.isPotion = true; if (MWBase::Environment::get().getWindowManager()->getFullHelp()) { - text += MWGui::ToolTips::getMiscString(ptr.getCellRef().getOwner(), "Owner"); - text += MWGui::ToolTips::getMiscString(ptr.getCellRef().getFaction(), "Faction"); + text += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); text += MWGui::ToolTips::getMiscString(ref->mBase->mScript, "Script"); } @@ -166,13 +167,8 @@ namespace MWClass MWWorld::LiveCellRef *ref = ptr.get(); - MWWorld::Ptr actor = MWBase::Environment::get().getWorld()->getPlayerPtr(); - - // remove used potion (assume it is present in inventory) - ptr.getContainerStore()->remove(ptr, 1, actor); - boost::shared_ptr action ( - new MWWorld::ActionApply (actor, ref->mBase->mId)); + new MWWorld::ActionApply (ptr, ref->mBase->mId)); action->setSound ("Drink"); diff --git a/apps/openmw/mwclass/potion.hpp b/apps/openmw/mwclass/potion.hpp index 0f0578ca0..4c407d161 100644 --- a/apps/openmw/mwclass/potion.hpp +++ b/apps/openmw/mwclass/potion.hpp @@ -12,6 +12,9 @@ namespace MWClass public: + /// Return ID of \a ptr + virtual std::string getId (const MWWorld::Ptr& ptr) const; + virtual void insertObjectRendering (const MWWorld::Ptr& ptr, MWRender::RenderingInterface& renderingInterface) const; ///< Add reference into a cell for rendering diff --git a/apps/openmw/mwclass/probe.cpp b/apps/openmw/mwclass/probe.cpp index 5d076a3c5..82f487986 100644 --- a/apps/openmw/mwclass/probe.cpp +++ b/apps/openmw/mwclass/probe.cpp @@ -22,6 +22,11 @@ namespace MWClass { + std::string Probe::getId (const MWWorld::Ptr& ptr) const + { + return ptr.get()->mBase->mId; + } + void Probe::insertObjectRendering (const MWWorld::Ptr& ptr, MWRender::RenderingInterface& renderingInterface) const { const std::string model = getModel(ptr); @@ -85,7 +90,7 @@ namespace MWClass MWWorld::LiveCellRef *ref = ptr.get(); - return ref->mBase->mData.mValue * (static_cast(getItemHealth(ptr)) / getItemMaxHealth(ptr)); + return ref->mBase->mData.mValue; } void Probe::registerSelf() @@ -137,11 +142,10 @@ namespace MWClass text += "\n#{sUses}: " + MWGui::ToolTips::toString(remainingUses); text += "\n#{sQuality}: " + MWGui::ToolTips::toString(ref->mBase->mData.mQuality); text += "\n#{sWeight}: " + MWGui::ToolTips::toString(ref->mBase->mData.mWeight); - text += MWGui::ToolTips::getValueString(getValue(ptr), "#{sValue}"); + text += MWGui::ToolTips::getValueString(ref->mBase->mData.mValue, "#{sValue}"); if (MWBase::Environment::get().getWindowManager()->getFullHelp()) { - text += MWGui::ToolTips::getMiscString(ptr.getCellRef().getOwner(), "Owner"); - text += MWGui::ToolTips::getMiscString(ptr.getCellRef().getFaction(), "Faction"); + text += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); text += MWGui::ToolTips::getMiscString(ref->mBase->mScript, "Script"); } diff --git a/apps/openmw/mwclass/probe.hpp b/apps/openmw/mwclass/probe.hpp index 832169f8b..047cb8ed4 100644 --- a/apps/openmw/mwclass/probe.hpp +++ b/apps/openmw/mwclass/probe.hpp @@ -12,6 +12,9 @@ namespace MWClass public: + /// Return ID of \a ptr + virtual std::string getId (const MWWorld::Ptr& ptr) const; + virtual void insertObjectRendering (const MWWorld::Ptr& ptr, MWRender::RenderingInterface& renderingInterface) const; ///< Add reference into a cell for rendering @@ -64,6 +67,9 @@ namespace MWClass virtual int getItemMaxHealth (const MWWorld::Ptr& ptr) const; ///< Return item max health or throw an exception, if class does not have item health + + virtual bool hasItemHealth (const MWWorld::Ptr& ptr) const { return true; } + ///< \return Item health data available? (default implementation: false) }; } diff --git a/apps/openmw/mwclass/repair.cpp b/apps/openmw/mwclass/repair.cpp index 9b528a4fc..4b18aced0 100644 --- a/apps/openmw/mwclass/repair.cpp +++ b/apps/openmw/mwclass/repair.cpp @@ -21,6 +21,11 @@ namespace MWClass { + std::string Repair::getId (const MWWorld::Ptr& ptr) const + { + return ptr.get()->mBase->mId; + } + void Repair::insertObjectRendering (const MWWorld::Ptr& ptr, MWRender::RenderingInterface& renderingInterface) const { const std::string model = getModel(ptr); @@ -76,7 +81,7 @@ namespace MWClass MWWorld::LiveCellRef *ref = ptr.get(); - return ref->mBase->mData.mValue * (static_cast(getItemHealth(ptr)) / getItemMaxHealth(ptr)); + return ref->mBase->mData.mValue; } void Repair::registerSelf() @@ -141,11 +146,10 @@ namespace MWClass text += "\n#{sUses}: " + MWGui::ToolTips::toString(remainingUses); text += "\n#{sQuality}: " + MWGui::ToolTips::toString(ref->mBase->mData.mQuality); text += "\n#{sWeight}: " + MWGui::ToolTips::toString(ref->mBase->mData.mWeight); - text += MWGui::ToolTips::getValueString(getValue(ptr), "#{sValue}"); + text += MWGui::ToolTips::getValueString(ref->mBase->mData.mValue, "#{sValue}"); if (MWBase::Environment::get().getWindowManager()->getFullHelp()) { - text += MWGui::ToolTips::getMiscString(ptr.getCellRef().getOwner(), "Owner"); - text += MWGui::ToolTips::getMiscString(ptr.getCellRef().getFaction(), "Faction"); + text += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); text += MWGui::ToolTips::getMiscString(ref->mBase->mScript, "Script"); } diff --git a/apps/openmw/mwclass/repair.hpp b/apps/openmw/mwclass/repair.hpp index 28ca5ad4c..f89258234 100644 --- a/apps/openmw/mwclass/repair.hpp +++ b/apps/openmw/mwclass/repair.hpp @@ -12,6 +12,9 @@ namespace MWClass public: + /// Return ID of \a ptr + virtual std::string getId (const MWWorld::Ptr& ptr) const; + virtual void insertObjectRendering (const MWWorld::Ptr& ptr, MWRender::RenderingInterface& renderingInterface) const; ///< Add reference into a cell for rendering @@ -60,7 +63,7 @@ namespace MWClass virtual int getItemMaxHealth (const MWWorld::Ptr& ptr) const; ///< Return item max health or throw an exception, if class does not have item health - /// (default implementation: throw an exceoption) + /// (default implementation: throw an exception) virtual float getWeight (const MWWorld::Ptr& ptr) const; diff --git a/apps/openmw/mwclass/static.cpp b/apps/openmw/mwclass/static.cpp index 4ac41350f..c241935ab 100644 --- a/apps/openmw/mwclass/static.cpp +++ b/apps/openmw/mwclass/static.cpp @@ -12,11 +12,19 @@ namespace MWClass { + std::string Static::getId (const MWWorld::Ptr& ptr) const + { + return ptr.get()->mBase->mId; + } + void Static::insertObjectRendering (const MWWorld::Ptr& ptr, MWRender::RenderingInterface& renderingInterface) const { + MWWorld::LiveCellRef *ref = + ptr.get(); + const std::string model = getModel(ptr); if (!model.empty()) { - renderingInterface.getObjects().insertModel(ptr, model); + renderingInterface.getObjects().insertModel(ptr, model, !ref->mBase->mPersistent); } } diff --git a/apps/openmw/mwclass/static.hpp b/apps/openmw/mwclass/static.hpp index e36b3d142..2ac2e8682 100644 --- a/apps/openmw/mwclass/static.hpp +++ b/apps/openmw/mwclass/static.hpp @@ -12,6 +12,9 @@ namespace MWClass public: + /// Return ID of \a ptr + virtual std::string getId (const MWWorld::Ptr& ptr) const; + virtual void insertObjectRendering (const MWWorld::Ptr& ptr, MWRender::RenderingInterface& renderingInterface) const; ///< Add reference into a cell for rendering diff --git a/apps/openmw/mwclass/weapon.cpp b/apps/openmw/mwclass/weapon.cpp index 26618c021..d2f88efef 100644 --- a/apps/openmw/mwclass/weapon.cpp +++ b/apps/openmw/mwclass/weapon.cpp @@ -154,7 +154,7 @@ namespace MWClass MWWorld::LiveCellRef *ref = ptr.get(); - return ref->mBase->mData.mValue * (static_cast(getItemHealth(ptr)) / getItemMaxHealth(ptr)); + return ref->mBase->mData.mValue; } void Weapon::registerSelf() @@ -343,7 +343,7 @@ namespace MWClass } text += "\n#{sWeight}: " + MWGui::ToolTips::toString(ref->mBase->mData.mWeight); - text += MWGui::ToolTips::getValueString(getValue(ptr), "#{sValue}"); + text += MWGui::ToolTips::getValueString(ref->mBase->mData.mValue, "#{sValue}"); info.enchant = ref->mBase->mEnchant; @@ -351,8 +351,7 @@ namespace MWClass info.remainingEnchantCharge = ptr.getCellRef().getEnchantmentCharge(); if (MWBase::Environment::get().getWindowManager()->getFullHelp()) { - text += MWGui::ToolTips::getMiscString(ptr.getCellRef().getOwner(), "Owner"); - text += MWGui::ToolTips::getMiscString(ptr.getCellRef().getFaction(), "Faction"); + text += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); text += MWGui::ToolTips::getMiscString(ref->mBase->mScript, "Script"); } @@ -379,6 +378,7 @@ namespace MWClass newItem.mName=newName; newItem.mData.mEnchant=enchCharge; newItem.mEnchant=enchId; + newItem.mData.mFlags |= ESM::Weapon::Magical; const ESM::Weapon *record = MWBase::Environment::get().getWorld()->createRecord (newItem); return record->mId; } @@ -435,7 +435,8 @@ namespace MWClass bool Weapon::canSell (const MWWorld::Ptr& item, int npcServices) const { - return npcServices & ESM::NPC::Weapon; + return (npcServices & ESM::NPC::Weapon) + || ((npcServices & ESM::NPC::MagicItems) && !getEnchantment(item).empty()); } float Weapon::getWeight(const MWWorld::Ptr &ptr) const diff --git a/apps/openmw/mwdialogue/dialoguemanagerimp.cpp b/apps/openmw/mwdialogue/dialoguemanagerimp.cpp index b6cef2fe7..eff54fbc0 100644 --- a/apps/openmw/mwdialogue/dialoguemanagerimp.cpp +++ b/apps/openmw/mwdialogue/dialoguemanagerimp.cpp @@ -42,6 +42,7 @@ #include "../mwmechanics/npcstats.hpp" #include "filter.hpp" +#include "hypertextparser.hpp" namespace MWDialogue { @@ -82,42 +83,27 @@ namespace MWDialogue void DialogueManager::parseText (const std::string& text) { - std::vector hypertext = ParseHyperText(text); + std::vector hypertext = HyperTextParser::parseHyperText(text); - //calculation of standard form fir all hyperlinks - for (size_t i = 0; i < hypertext.size(); ++i) + for (std::vector::iterator tok = hypertext.begin(); tok != hypertext.end(); ++tok) { - if (hypertext[i].mLink) + std::string topicId = Misc::StringUtils::lowerCase(tok->mText); + + if (tok->isExplicitLink()) { - size_t asterisk_count = MWDialogue::RemovePseudoAsterisks(hypertext[i].mText); + // calculation of standard form for all hyperlinks + size_t asterisk_count = HyperTextParser::removePseudoAsterisks(topicId); for(; asterisk_count > 0; --asterisk_count) - hypertext[i].mText.append("*"); + topicId.append("*"); - hypertext[i].mText = mTranslationDataStorage.topicStandardForm(hypertext[i].mText); + topicId = mTranslationDataStorage.topicStandardForm(topicId); } - } - for (size_t i = 0; i < hypertext.size(); ++i) - { - std::list::iterator it; - for(it = mActorKnownTopics.begin(); it != mActorKnownTopics.end(); ++it) - { - if (hypertext[i].mLink) - { - if( hypertext[i].mText == *it ) - { - mKnownTopics[hypertext[i].mText] = true; - } - } - else if( !mTranslationDataStorage.hasTranslation() ) - { - size_t pos = Misc::StringUtils::lowerCase(hypertext[i].mText).find(*it, 0); - if(pos !=std::string::npos) - { - mKnownTopics[*it] = true; - } - } - } + if (tok->isImplicitKeyword() && mTranslationDataStorage.hasTranslation()) + continue; + + if (std::find(mActorKnownTopics.begin(), mActorKnownTopics.end(), topicId) != mActorKnownTopics.end()) + mKnownTopics[topicId] = true; } updateTopics(); @@ -125,6 +111,10 @@ namespace MWDialogue void DialogueManager::startDialogue (const MWWorld::Ptr& actor) { + // Dialogue with dead actor (e.g. through script) should not be allowed. + if (actor.getClass().getCreatureStats(actor).isDead()) + return; + mLastTopic = ""; mPermanentDispositionChange = 0; mTemporaryDispositionChange = 0; @@ -140,7 +130,11 @@ namespace MWDialogue mActorKnownTopics.clear(); MWGui::DialogueWindow* win = MWBase::Environment::get().getWindowManager()->getDialogueWindow(); - win->startDialogue(actor, actor.getClass().getName (actor)); + + // If the dialogue window was already open, keep the existing history + bool resetHistory = (!MWBase::Environment::get().getWindowManager()->containsMode(MWGui::GM_Dialogue)); + + win->startDialogue(actor, actor.getClass().getName (actor), resetHistory); //setup the list of topics known by the actor. Topics who are also on the knownTopics list will be added to the GUI updateTopics(); @@ -174,10 +168,20 @@ namespace MWDialogue win->addResponse (Interpreter::fixDefinesDialog(info->mResponse, interpreterContext)); executeScript (info->mResultScript); mLastTopic = Misc::StringUtils::lowerCase(it->mId); - break; + return; } } } + + // No greetings found. The dialogue window should not be shown. + // If this is a companion, we must show the companion window directly (used by BM_bear_be_unique). + bool isCompanion = !mActor.getClass().getScript(mActor).empty() + && mActor.getRefData().getLocals().getIntVar(mActor.getClass().getScript(mActor), "companion"); + if (isCompanion) + { + MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_Companion); + MWBase::Environment::get().getWindowManager()->showCompanionWindow(mActor); + } } bool DialogueManager::compile (const std::string& cmd,std::vector& code) @@ -248,7 +252,7 @@ namespace MWDialogue } catch (const std::exception& error) { - std::cerr << std::string ("Dialogue error: An exception has been thrown: ") + error.what(); + std::cerr << std::string ("Dialogue error: An exception has been thrown: ") + error.what() << std::endl; } } } @@ -272,6 +276,9 @@ namespace MWDialogue std::string title; if (dialogue.mType==ESM::Dialogue::Persuasion) { + // Determine GMST from dialogue topic. GMSTs are: + // sAdmireSuccess, sAdmireFail, sIntimidateSuccess, sIntimidateFail, + // sTauntSuccess, sTauntFail, sBribeSuccess, sBribeFail std::string modifiedTopic = "s" + topic; modifiedTopic.erase (std::remove (modifiedTopic.begin(), modifiedTopic.end(), ' '), modifiedTopic.end()); @@ -294,7 +301,7 @@ namespace MWDialogue { if (iter->mId == info->mId) { - MWBase::Environment::get().getJournal()->addTopic (topic, info->mId, mActor.getClass().getName(mActor)); + MWBase::Environment::get().getJournal()->addTopic (topic, info->mId, mActor); break; } } @@ -398,7 +405,7 @@ namespace MWDialogue win->setServices (windowServices); // sort again, because the previous sort was case-sensitive - keywordList.sort(Misc::StringUtils::ciEqual); + keywordList.sort(Misc::StringUtils::ciLess); win->setKeywords(keywordList); mChoice = choice; @@ -472,7 +479,7 @@ namespace MWDialogue { if (iter->mId == info->mId) { - MWBase::Environment::get().getJournal()->addTopic (mLastTopic, info->mId, mActor.getClass().getName(mActor)); + MWBase::Environment::get().getJournal()->addTopic (mLastTopic, info->mId, mActor); break; } } @@ -487,9 +494,10 @@ namespace MWDialogue void DialogueManager::askQuestion (const std::string& question, int choice) { + mIsInChoice = true; + MWGui::DialogueWindow* win = MWBase::Environment::get().getWindowManager()->getDialogueWindow(); win->addChoice(question, choice); - mIsInChoice = true; } void DialogueManager::goodbye() @@ -560,17 +568,7 @@ namespace MWDialogue void DialogueManager::applyDispositionChange(int delta) { - int oldTemp = mTemporaryDispositionChange; mTemporaryDispositionChange += delta; - // don't allow increasing beyond 100 or decreasing below 0 - int curDisp = MWBase::Environment::get().getMechanicsManager()->getDerivedDisposition(mActor); - if (curDisp + mTemporaryDispositionChange < 0) - mTemporaryDispositionChange = -curDisp; - else if (curDisp + mTemporaryDispositionChange > 100) - mTemporaryDispositionChange = 100 - curDisp; - - int diff = mTemporaryDispositionChange - oldTemp; - mPermanentDispositionChange += diff; } bool DialogueManager::checkServiceRefused() @@ -704,54 +702,12 @@ namespace MWDialogue return diff; } - std::vector ParseHyperText(const std::string& text) + void DialogueManager::clearInfoActor(const MWWorld::Ptr &actor) const { - std::vector result; - MyGUI::UString utext(text); - size_t pos_begin, pos_end, iteration_pos = 0; - for(;;) + if (actor == mActor && !mLastTopic.empty()) { - pos_begin = utext.find('@', iteration_pos); - if (pos_begin != std::string::npos) - pos_end = utext.find('#', pos_begin); - - if (pos_begin != std::string::npos && pos_end != std::string::npos) - { - result.push_back( HyperTextToken(utext.substr(iteration_pos, pos_begin - iteration_pos), false) ); - - std::string link = utext.substr(pos_begin + 1, pos_end - pos_begin - 1); - result.push_back( HyperTextToken(link, true) ); - - iteration_pos = pos_end + 1; - } - else - { - result.push_back( HyperTextToken(utext.substr(iteration_pos), false) ); - break; - } + MWBase::Environment::get().getJournal()->removeLastAddedTopicResponse( + mLastTopic, actor.getClass().getName(actor)); } - - return result; - } - - size_t RemovePseudoAsterisks(std::string& phrase) - { - size_t pseudoAsterisksCount = 0; - const char specialPseudoAsteriskCharacter = 127; - - if( !phrase.empty() ) - { - std::string::reverse_iterator rit = phrase.rbegin(); - - while( rit != phrase.rend() && *rit == specialPseudoAsteriskCharacter ) - { - pseudoAsterisksCount++; - ++rit; - } - } - - phrase = phrase.substr(0, phrase.length() - pseudoAsterisksCount); - - return pseudoAsterisksCount; } } diff --git a/apps/openmw/mwdialogue/dialoguemanagerimp.hpp b/apps/openmw/mwdialogue/dialoguemanagerimp.hpp index db0b78d59..9c3d0d54b 100644 --- a/apps/openmw/mwdialogue/dialoguemanagerimp.hpp +++ b/apps/openmw/mwdialogue/dialoguemanagerimp.hpp @@ -40,7 +40,7 @@ namespace MWDialogue bool mTalkedTo; int mChoice; - std::string mLastTopic; + std::string mLastTopic; // last topic ID, lowercase bool mIsInChoice; float mTemporaryDispositionChange; @@ -84,6 +84,8 @@ namespace MWDialogue virtual void persuade (int type); virtual int getTemporaryDispositionChange () const; + + /// @note This change is temporary and gets discarded when dialogue ends. virtual void applyDispositionChange (int delta); virtual int countSavedGameRecords() const; @@ -97,22 +99,10 @@ namespace MWDialogue /// @return faction1's opinion of faction2 virtual int getFactionReaction (const std::string& faction1, const std::string& faction2) const; - }; - - struct HyperTextToken - { - HyperTextToken(const std::string& text, bool link) : mText(text), mLink(link) {} - - std::string mText; - bool mLink; + /// Removes the last added topic response for the given actor from the journal + virtual void clearInfoActor (const MWWorld::Ptr& actor) const; }; - - // In translations (at least Russian) the links are marked with @#, so - // it should be a function to parse it - std::vector ParseHyperText(const std::string& text); - - size_t RemovePseudoAsterisks(std::string& phrase); } #endif diff --git a/apps/openmw/mwdialogue/filter.cpp b/apps/openmw/mwdialogue/filter.cpp index d301e88aa..629d99cc2 100644 --- a/apps/openmw/mwdialogue/filter.cpp +++ b/apps/openmw/mwdialogue/filter.cpp @@ -1,11 +1,14 @@ #include "filter.hpp" +#include + #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/journal.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/dialoguemanager.hpp" +#include "../mwbase/scriptmanager.hpp" #include "../mwworld/class.hpp" #include "../mwworld/inventorystore.hpp" @@ -65,7 +68,7 @@ bool MWDialogue::Filter::testActor (const ESM::DialInfo& info) const return false; MWMechanics::NpcStats& stats = mActor.getClass().getNpcStats (mActor); - std::map::iterator iter = stats.getFactionRanks().find ( Misc::StringUtils::lowerCase (info.mFaction)); + std::map::const_iterator iter = stats.getFactionRanks().find ( Misc::StringUtils::lowerCase (info.mFaction)); if (iter==stats.getFactionRanks().end()) return false; @@ -76,8 +79,18 @@ bool MWDialogue::Filter::testActor (const ESM::DialInfo& info) const } else if (info.mData.mRank != -1) { - // if there is a rank condition, but the NPC is not in a faction, always fail - return false; + if (isCreature) + return false; + + // Rank requirement, but no faction given. Use the actor's faction, if there is one. + MWMechanics::NpcStats& stats = mActor.getClass().getNpcStats (mActor); + + if (!stats.getFactionRanks().size()) + return false; + + // check rank + if (stats.getFactionRanks().begin()->second < info.mData.mRank) + return false; } // Gender @@ -99,7 +112,7 @@ bool MWDialogue::Filter::testPlayer (const ESM::DialInfo& info) const if (!info.mPcFaction.empty()) { MWMechanics::NpcStats& stats = player.getClass().getNpcStats (player); - std::map::iterator iter = stats.getFactionRanks().find (Misc::StringUtils::lowerCase (info.mPcFaction)); + std::map::const_iterator iter = stats.getFactionRanks().find (Misc::StringUtils::lowerCase (info.mPcFaction)); if(iter==stats.getFactionRanks().end()) return false; @@ -187,33 +200,28 @@ bool MWDialogue::Filter::testSelectStructNumeric (const SelectWrapper& select) c if (scriptName.empty()) return false; // no script - const ESM::Script *script = - MWBase::Environment::get().getWorld()->getStore().get().find (scriptName); + std::string name = Misc::StringUtils::lowerCase (select.getName()); - std::string name = select.getName(); + const Compiler::Locals& localDefs = + MWBase::Environment::get().getScriptManager()->getLocals (scriptName); - int i = 0; + char type = localDefs.getType (name); - for (; i (script->mVarNames.size()); ++i) - if (Misc::StringUtils::ciEqual(script->mVarNames[i], name)) - break; + if (type==' ') + return false; // script does not have a variable of this name. - if (i>=static_cast (script->mVarNames.size())) - return false; // script does not have a variable of this name + int index = localDefs.getIndex (name); const MWScript::Locals& locals = mActor.getRefData().getLocals(); - if (imData.mNumShorts) - return select.selectCompare (static_cast (locals.mShorts[i])); - - i -= script->mData.mNumShorts; - - if (imData.mNumLongs) - return select.selectCompare (locals.mLongs[i]); - - i -= script->mData.mNumLongs; + switch (type) + { + case 's': return select.selectCompare (static_cast (locals.mShorts[index])); + case 'l': return select.selectCompare (locals.mLongs[index]); + case 'f': return select.selectCompare (locals.mFloats[index]); + } - return select.selectCompare (locals.mFloats.at (i)); + throw std::logic_error ("unknown local variable type in dialogue filter"); } case SelectWrapper::Function_PcHealthPercent: @@ -241,7 +249,7 @@ bool MWDialogue::Filter::testSelectStructNumeric (const SelectWrapper& select) c float ratio = mActor.getClass().getCreatureStats (mActor).getHealth().getCurrent() / mActor.getClass().getCreatureStats (mActor).getHealth().getModified(); - return select.selectCompare (ratio); + return select.selectCompare (static_cast(ratio*100)); } default: @@ -371,7 +379,7 @@ int MWDialogue::Filter::getSelectStructInteger (const SelectWrapper& select) con if (mActor.getClass().getNpcStats (mActor).getFactionRanks().empty()) return 0; - std::pair faction = + const std::pair faction = *mActor.getClass().getNpcStats (mActor).getFactionRanks().begin(); int rank = getFactionRank (player, faction.first); @@ -453,20 +461,10 @@ bool MWDialogue::Filter::getSelectStructBoolean (const SelectWrapper& select) co // This actor has no attached script, so there is no local variable return true; - const ESM::Script *script = - MWBase::Environment::get().getWorld()->getStore().get().find (scriptName); + const Compiler::Locals& localDefs = + MWBase::Environment::get().getScriptManager()->getLocals (scriptName); - std::string name = select.getName(); - - int i = 0; - for (; i < static_cast (script->mVarNames.size()); ++i) - if (Misc::StringUtils::ciEqual(script->mVarNames[i], name)) - break; - - if (i >= static_cast (script->mVarNames.size())) - return true; // script does not have a variable of this name - - return false; + return localDefs.getIndex (Misc::StringUtils::lowerCase (select.getName()))==-1; } case SelectWrapper::Function_SameGender: @@ -494,7 +492,7 @@ bool MWDialogue::Filter::getSelectStructBoolean (const SelectWrapper& select) co case SelectWrapper::Function_PcCorprus: return player.getClass().getCreatureStats (player). - getMagicEffects().get (ESM::MagicEffect::Corprus).mMagnitude!=0; + getMagicEffects().get (ESM::MagicEffect::Corprus).getMagnitude()!=0; case SelectWrapper::Function_PcExpelled: { @@ -510,7 +508,7 @@ bool MWDialogue::Filter::getSelectStructBoolean (const SelectWrapper& select) co case SelectWrapper::Function_PcVampire: return player.getClass().getCreatureStats(player).getMagicEffects(). - get(ESM::MagicEffect::Vampirism).mMagnitude > 0; + get(ESM::MagicEffect::Vampirism).getMagnitude() > 0; case SelectWrapper::Function_TalkedToPc: @@ -530,7 +528,8 @@ bool MWDialogue::Filter::getSelectStructBoolean (const SelectWrapper& select) co case SelectWrapper::Function_ShouldAttack: - return mActor.getClass().getCreatureStats(mActor).getAiSetting(MWMechanics::CreatureStats::AI_Fight).getModified() >= 80; + return MWBase::Environment::get().getMechanicsManager()->isAggressive(mActor, + MWBase::Environment::get().getWorld()->getPlayerPtr()); case SelectWrapper::Function_CreatureTargetted: diff --git a/apps/openmw/mwdialogue/hypertextparser.cpp b/apps/openmw/mwdialogue/hypertextparser.cpp new file mode 100644 index 000000000..776edac94 --- /dev/null +++ b/apps/openmw/mwdialogue/hypertextparser.cpp @@ -0,0 +1,89 @@ +#include + +#include "../mwbase/environment.hpp" +#include "../mwbase/world.hpp" + +#include "../mwworld/store.hpp" +#include "../mwworld/esmstore.hpp" + +#include "keywordsearch.hpp" + +#include "hypertextparser.hpp" + +namespace MWDialogue +{ + namespace HyperTextParser + { + std::vector parseHyperText(const std::string & text) + { + std::vector result; + size_t pos_end, iteration_pos = 0; + for(;;) + { + size_t pos_begin = text.find('@', iteration_pos); + if (pos_begin != std::string::npos) + pos_end = text.find('#', pos_begin); + + if (pos_begin != std::string::npos && pos_end != std::string::npos) + { + if (pos_begin != iteration_pos) + tokenizeKeywords(text.substr(iteration_pos, pos_begin - iteration_pos), result); + + std::string link = text.substr(pos_begin + 1, pos_end - pos_begin - 1); + result.push_back(Token(link, Token::ExplicitLink)); + + iteration_pos = pos_end + 1; + } + else + { + if (iteration_pos != text.size()) + tokenizeKeywords(text.substr(iteration_pos), result); + break; + } + } + + return result; + } + + void tokenizeKeywords(const std::string & text, std::vector & tokens) + { + const MWWorld::Store & dialogs = + MWBase::Environment::get().getWorld()->getStore().get(); + + std::list keywordList; + for (MWWorld::Store::iterator it = dialogs.begin(); it != dialogs.end(); ++it) + keywordList.push_back(Misc::StringUtils::lowerCase(it->mId)); + keywordList.sort(Misc::StringUtils::ciLess); + + KeywordSearch keywordSearch; + KeywordSearch::Match match; + + for (std::list::const_iterator it = keywordList.begin(); it != keywordList.end(); ++it) + keywordSearch.seed(*it, 0 /*unused*/); + + for (std::string::const_iterator it = text.begin(); it != text.end() && keywordSearch.search(it, text.end(), match, text.begin()); it = match.mEnd) + tokens.push_back(Token(std::string(match.mBeg, match.mEnd), Token::ImplicitKeyword)); + } + + size_t removePseudoAsterisks(std::string & phrase) + { + size_t pseudoAsterisksCount = 0; + + if( !phrase.empty() ) + { + std::string::reverse_iterator rit = phrase.rbegin(); + + const char specialPseudoAsteriskCharacter = 127; + while( rit != phrase.rend() && *rit == specialPseudoAsteriskCharacter ) + { + pseudoAsterisksCount++; + ++rit; + } + } + + phrase = phrase.substr(0, phrase.length() - pseudoAsterisksCount); + + return pseudoAsterisksCount; + } + } +} diff --git a/apps/openmw/mwdialogue/hypertextparser.hpp b/apps/openmw/mwdialogue/hypertextparser.hpp new file mode 100644 index 000000000..13e135f3c --- /dev/null +++ b/apps/openmw/mwdialogue/hypertextparser.hpp @@ -0,0 +1,36 @@ +#ifndef GAME_MWDIALOGUE_HYPERTEXTPARSER_H +#define GAME_MWDIALOGUE_HYPERTEXTPARSER_H + +#include +#include + +namespace MWDialogue +{ + namespace HyperTextParser + { + struct Token + { + enum Type + { + ExplicitLink, // enclosed in @# + ImplicitKeyword + }; + + Token(const std::string & text, Type type) : mText(text), mType(type) {} + + bool isExplicitLink() { return mType == ExplicitLink; } + bool isImplicitKeyword() { return mType == ImplicitKeyword; } + + std::string mText; + Type mType; + }; + + // In translations (at least Russian) the links are marked with @#, so + // it should be a function to parse it + std::vector parseHyperText(const std::string & text); + void tokenizeKeywords(const std::string & text, std::vector & tokens); + size_t removePseudoAsterisks(std::string & phrase); + } +} + +#endif diff --git a/apps/openmw/mwdialogue/journalentry.cpp b/apps/openmw/mwdialogue/journalentry.cpp index 55574bf3e..b92d7cace 100644 --- a/apps/openmw/mwdialogue/journalentry.cpp +++ b/apps/openmw/mwdialogue/journalentry.cpp @@ -5,16 +5,21 @@ #include +#include + #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwworld/esmstore.hpp" +#include "../mwscript/interpretercontext.hpp" + + namespace MWDialogue { Entry::Entry() {} - Entry::Entry (const std::string& topic, const std::string& infoId) + Entry::Entry (const std::string& topic, const std::string& infoId, const MWWorld::Ptr& actor) : mInfoId (infoId) { const ESM::Dialogue *dialogue = @@ -24,8 +29,17 @@ namespace MWDialogue iter!=dialogue->mInfo.end(); ++iter) if (iter->mId == mInfoId) { - /// \todo text replacement - mText = iter->mResponse; + if (actor.isEmpty()) + { + MWScript::InterpreterContext interpreterContext(NULL,MWWorld::Ptr()); + mText = Interpreter::fixDefinesDialog(iter->mResponse, interpreterContext); + } + else + { + MWScript::InterpreterContext interpreterContext(&actor.getRefData().getLocals(),actor); + mText = Interpreter::fixDefinesDialog(iter->mResponse, interpreterContext); + } + return; } @@ -49,8 +63,8 @@ namespace MWDialogue JournalEntry::JournalEntry() {} - JournalEntry::JournalEntry (const std::string& topic, const std::string& infoId) - : Entry (topic, infoId), mTopic (topic) + JournalEntry::JournalEntry (const std::string& topic, const std::string& infoId, const MWWorld::Ptr& actor) + : Entry (topic, infoId, actor), mTopic (topic) {} JournalEntry::JournalEntry (const ESM::JournalEntry& record) @@ -65,7 +79,7 @@ namespace MWDialogue JournalEntry JournalEntry::makeFromQuest (const std::string& topic, int index) { - return JournalEntry (topic, idFromIndex (topic, index)); + return JournalEntry (topic, idFromIndex (topic, index), MWWorld::Ptr()); } std::string JournalEntry::idFromIndex (const std::string& topic, int index) @@ -90,7 +104,7 @@ namespace MWDialogue StampedJournalEntry::StampedJournalEntry (const std::string& topic, const std::string& infoId, int day, int month, int dayOfMonth) - : JournalEntry (topic, infoId), mDay (day), mMonth (month), mDayOfMonth (dayOfMonth) + : JournalEntry (topic, infoId, MWWorld::Ptr()), mDay (day), mMonth (month), mDayOfMonth (dayOfMonth) {} StampedJournalEntry::StampedJournalEntry (const ESM::JournalEntry& record) diff --git a/apps/openmw/mwdialogue/journalentry.hpp b/apps/openmw/mwdialogue/journalentry.hpp index a77ba4f7c..3ae3efcc8 100644 --- a/apps/openmw/mwdialogue/journalentry.hpp +++ b/apps/openmw/mwdialogue/journalentry.hpp @@ -8,6 +8,11 @@ namespace ESM struct JournalEntry; } +namespace MWWorld +{ + class Ptr; +} + namespace MWDialogue { /// \brief Basic quest/dialogue/topic entry @@ -19,7 +24,8 @@ namespace MWDialogue Entry(); - Entry (const std::string& topic, const std::string& infoId); + /// actor is optional + Entry (const std::string& topic, const std::string& infoId, const MWWorld::Ptr& actor); Entry (const ESM::JournalEntry& record); @@ -37,7 +43,7 @@ namespace MWDialogue JournalEntry(); - JournalEntry (const std::string& topic, const std::string& infoId); + JournalEntry (const std::string& topic, const std::string& infoId, const MWWorld::Ptr& actor); JournalEntry (const ESM::JournalEntry& record); diff --git a/apps/openmw/mwdialogue/journalimp.cpp b/apps/openmw/mwdialogue/journalimp.cpp index 04aa0534d..9497347e3 100644 --- a/apps/openmw/mwdialogue/journalimp.cpp +++ b/apps/openmw/mwdialogue/journalimp.cpp @@ -9,6 +9,7 @@ #include #include "../mwworld/esmstore.hpp" +#include "../mwworld/class.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" @@ -103,15 +104,25 @@ namespace MWDialogue quest.setIndex (index); } - void Journal::addTopic (const std::string& topicId, const std::string& infoId, const std::string& actorName) + void Journal::addTopic (const std::string& topicId, const std::string& infoId, const MWWorld::Ptr& actor) { Topic& topic = getTopic (topicId); - JournalEntry entry(topicId, infoId); - entry.mActorName = actorName; + JournalEntry entry(topicId, infoId, actor); + entry.mActorName = actor.getClass().getName(actor); topic.addEntry (entry); } + void Journal::removeLastAddedTopicResponse(const std::string &topicId, const std::string &actorName) + { + Topic& topic = getTopic (topicId); + + topic.removeLastAddedResponse(actorName); + + if (topic.begin() == topic.end()) + mTopics.erase(mTopics.find(topicId)); // All responses removed -> remove topic + } + int Journal::getJournalIndex (const std::string& id) const { TQuestContainer::const_iterator iter = mQuests.find (id); diff --git a/apps/openmw/mwdialogue/journalimp.hpp b/apps/openmw/mwdialogue/journalimp.hpp index 00511f47c..d15b909fa 100644 --- a/apps/openmw/mwdialogue/journalimp.hpp +++ b/apps/openmw/mwdialogue/journalimp.hpp @@ -38,7 +38,12 @@ namespace MWDialogue virtual int getJournalIndex (const std::string& id) const; ///< Get the journal index. - virtual void addTopic (const std::string& topicId, const std::string& infoId, const std::string& actorName); + virtual void addTopic (const std::string& topicId, const std::string& infoId, const MWWorld::Ptr& actor); + /// \note topicId must be lowercase + + virtual void removeLastAddedTopicResponse (const std::string& topicId, const std::string& actorName); + ///< Removes the last topic response added for the given topicId and actor name. + /// \note topicId must be lowercase virtual TEntryIter begin() const; ///< Iterator pointing to the begin of the main journal. diff --git a/apps/openmw/mwgui/keywordsearch.cpp b/apps/openmw/mwdialogue/keywordsearch.cpp similarity index 100% rename from apps/openmw/mwgui/keywordsearch.cpp rename to apps/openmw/mwdialogue/keywordsearch.cpp diff --git a/apps/openmw/mwgui/keywordsearch.hpp b/apps/openmw/mwdialogue/keywordsearch.hpp similarity index 98% rename from apps/openmw/mwgui/keywordsearch.hpp rename to apps/openmw/mwdialogue/keywordsearch.hpp index 4f4459b35..51508890c 100644 --- a/apps/openmw/mwgui/keywordsearch.hpp +++ b/apps/openmw/mwdialogue/keywordsearch.hpp @@ -1,5 +1,5 @@ -#ifndef MWGUI_KEYWORDSEARCH_H -#define MWGUI_KEYWORDSEARCH_H +#ifndef GAME_MWDIALOGUE_KEYWORDSEARCH_H +#define GAME_MWDIALOGUE_KEYWORDSEARCH_H #include #include @@ -9,7 +9,7 @@ #include -namespace MWGui +namespace MWDialogue { template @@ -141,7 +141,6 @@ public: match.mValue = candidate->second.mValue; match.mBeg = i; match.mEnd = k; - return true; } } diff --git a/apps/openmw/mwdialogue/topic.cpp b/apps/openmw/mwdialogue/topic.cpp index f7df305c7..c1a45f841 100644 --- a/apps/openmw/mwdialogue/topic.cpp +++ b/apps/openmw/mwdialogue/topic.cpp @@ -59,8 +59,16 @@ namespace MWDialogue return mEntries.end(); } - JournalEntry Topic::getEntry (const std::string& infoId) const + void Topic::removeLastAddedResponse (const std::string& actorName) { - return JournalEntry (mTopic, infoId); + for (std::vector::reverse_iterator it = mEntries.rbegin(); + it != mEntries.rend(); ++it) + { + if (it->mActorName == actorName) + { + mEntries.erase( (++it).base() ); // erase doesn't take a reverse_iterator + return; + } + } } } diff --git a/apps/openmw/mwdialogue/topic.hpp b/apps/openmw/mwdialogue/topic.hpp index 02fa6d524..72486ef8a 100644 --- a/apps/openmw/mwdialogue/topic.hpp +++ b/apps/openmw/mwdialogue/topic.hpp @@ -48,13 +48,13 @@ namespace MWDialogue virtual std::string getName() const; + void removeLastAddedResponse (const std::string& actorName); + TEntryIter begin() const; ///< Iterator pointing to the begin of the journal for this topic. TEntryIter end() const; ///< Iterator pointing past the end of the journal for this topic. - - JournalEntry getEntry (const std::string& infoId) const; }; } diff --git a/apps/openmw/mwgui/alchemywindow.cpp b/apps/openmw/mwgui/alchemywindow.cpp index 6524c168e..b9e0044ce 100644 --- a/apps/openmw/mwgui/alchemywindow.cpp +++ b/apps/openmw/mwgui/alchemywindow.cpp @@ -8,25 +8,14 @@ #include "../mwbase/soundmanager.hpp" #include "../mwbase/windowmanager.hpp" +#include "../mwmechanics/magiceffects.hpp" + #include "../mwworld/class.hpp" #include "inventoryitemmodel.hpp" #include "sortfilteritemmodel.hpp" #include "itemview.hpp" - -namespace -{ - std::string getIconPath(MWWorld::Ptr ptr) - { - std::string path = std::string("icons\\"); - path += ptr.getClass().getInventoryIcon(ptr); - int pos = path.rfind("."); - path.erase(pos); - path.append(".dds"); - return path; - } - -} +#include "itemwidget.hpp" namespace MWGui { @@ -66,10 +55,7 @@ namespace MWGui void AlchemyWindow::onCancelButtonClicked(MyGUI::Widget* _sender) { - mAlchemy.clear(); - - MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Alchemy); - MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Inventory); + exit(); } void AlchemyWindow::onCreateButtonClicked(MyGUI::Widget* _sender) @@ -148,17 +134,24 @@ namespace MWGui for (MWMechanics::Alchemy::TToolsIterator iter (mAlchemy.beginTools()); iter!=mAlchemy.endTools() && index (mApparatus.size()); ++iter, ++index) { + mApparatus.at (index)->setItem(*iter); + mApparatus.at (index)->clearUserStrings(); if (!iter->isEmpty()) { mApparatus.at (index)->setUserString ("ToolTipType", "ItemPtr"); mApparatus.at (index)->setUserData (*iter); - mApparatus.at (index)->setImageTexture (getIconPath (*iter)); } } update(); } + void AlchemyWindow::exit() { + mAlchemy.clear(); + MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Alchemy); + MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Inventory); + } + void AlchemyWindow::onIngredientSelected(MyGUI::Widget* _sender) { removeIngredient(_sender); @@ -181,12 +174,17 @@ namespace MWGui void AlchemyWindow::update() { + std::string suggestedName = mAlchemy.suggestPotionName(); + if (suggestedName != mSuggestedPotionName) + mNameEdit->setCaptionWithReplacing(suggestedName); + mSuggestedPotionName = suggestedName; + mSortModel->clearDragItems(); MWMechanics::Alchemy::TIngredientsIterator it = mAlchemy.beginIngredients (); for (int i=0; i<4; ++i) { - MyGUI::ImageBox* ingredient = mIngredients[i]; + ItemWidget* ingredient = mIngredients[i]; MWWorld::Ptr item; if (it != mAlchemy.endIngredients ()) @@ -201,32 +199,36 @@ namespace MWGui if (ingredient->getChildCount()) MyGUI::Gui::getInstance().destroyWidget(ingredient->getChildAt(0)); - ingredient->setImageTexture(""); ingredient->clearUserStrings (); + ingredient->setItem(item); + if (item.isEmpty ()) continue; ingredient->setUserString("ToolTipType", "ItemPtr"); ingredient->setUserData(item); - ingredient->setImageTexture(getIconPath(item)); - - MyGUI::TextBox* text = ingredient->createWidget("SandBrightText", MyGUI::IntCoord(0, 14, 32, 18), MyGUI::Align::Default, std::string("Label")); - text->setTextAlign(MyGUI::Align::Right); - text->setNeedMouseFocus(false); - text->setTextShadow(true); - text->setTextShadowColour(MyGUI::Colour(0,0,0)); - text->setCaption(ItemView::getCountString(ingredient->getUserData()->getRefData().getCount())); + + ingredient->setCount(ingredient->getUserData()->getRefData().getCount()); } mItemView->update(); - std::vector effects; - ESM::EffectList list; - list.mList = effects; - for (MWMechanics::Alchemy::TEffectsIterator it = mAlchemy.beginEffects (); it != mAlchemy.endEffects (); ++it) + std::set effectIds = mAlchemy.listEffects(); + Widgets::SpellEffectList list; + for (std::set::iterator it = effectIds.begin(); it != effectIds.end(); ++it) { - list.mList.push_back(*it); + Widgets::SpellEffectParams params; + params.mEffectID = it->mId; + const ESM::MagicEffect* magicEffect = MWBase::Environment::get().getWorld()->getStore().get().find(it->mId); + if (magicEffect->mData.mFlags & ESM::MagicEffect::TargetSkill) + params.mSkill = it->mArg; + else if (magicEffect->mData.mFlags & ESM::MagicEffect::TargetAttribute) + params.mAttribute = it->mArg; + params.mIsConstant = true; + params.mNoTarget = true; + + list.push_back(params); } while (mEffectsBox->getChildCount()) @@ -236,8 +238,7 @@ namespace MWGui Widgets::MWEffectListPtr effectsWidget = mEffectsBox->createWidget ("MW_StatName", coord, MyGUI::Align::Left | MyGUI::Align::Top); - Widgets::SpellEffectList _list = Widgets::MWEffectList::effectListFromESM(&list); - effectsWidget->setEffectList(_list); + effectsWidget->setEffectList(list); std::vector effectItems; effectsWidget->createEffectWidgets(effectItems, mEffectsBox, coord, false, 0); diff --git a/apps/openmw/mwgui/alchemywindow.hpp b/apps/openmw/mwgui/alchemywindow.hpp index 3a58ebf06..36f540c1b 100644 --- a/apps/openmw/mwgui/alchemywindow.hpp +++ b/apps/openmw/mwgui/alchemywindow.hpp @@ -11,6 +11,7 @@ namespace MWGui { class ItemView; + class ItemWidget; class SortFilterItemModel; class AlchemyWindow : public WindowBase @@ -19,8 +20,11 @@ namespace MWGui AlchemyWindow(); virtual void open(); + virtual void exit(); private: + std::string mSuggestedPotionName; + ItemView* mItemView; SortFilterItemModel* mSortModel; @@ -43,8 +47,8 @@ namespace MWGui MWMechanics::Alchemy mAlchemy; - std::vector mApparatus; - std::vector mIngredients; + std::vector mApparatus; + std::vector mIngredients; }; } diff --git a/apps/openmw/mwgui/birth.cpp b/apps/openmw/mwgui/birth.cpp index 7f58309ba..a7f90c00b 100644 --- a/apps/openmw/mwgui/birth.cpp +++ b/apps/openmw/mwgui/birth.cpp @@ -2,6 +2,8 @@ #include +#include + #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" @@ -182,9 +184,7 @@ namespace MWGui const ESM::BirthSign *birth = store.get().find(mCurrentBirthId); - std::string texturePath = std::string("textures\\") + birth->mTexture; - Widgets::fixTexturePath(texturePath); - mBirthImage->setImageTexture(texturePath); + mBirthImage->setImageTexture(Misc::ResourceHelpers::correctTexturePath(birth->mTexture)); std::vector abilities, powers, spells; diff --git a/apps/openmw/mwgui/birth.hpp b/apps/openmw/mwgui/birth.hpp index 20a64c78c..257dc6fef 100644 --- a/apps/openmw/mwgui/birth.hpp +++ b/apps/openmw/mwgui/birth.hpp @@ -35,6 +35,11 @@ namespace MWGui */ EventHandle_Void eventBack; + /** Event : Dialog finished, OK button clicked.\n + signature : void method()\n + */ + EventHandle_WindowBase eventDone; + protected: void onSelectBirth(MyGUI::ListBox* _sender, size_t _index); diff --git a/apps/openmw/mwgui/bookpage.cpp b/apps/openmw/mwgui/bookpage.cpp index 2cebc8e19..57cb3e7a9 100644 --- a/apps/openmw/mwgui/bookpage.cpp +++ b/apps/openmw/mwgui/bookpage.cpp @@ -360,10 +360,16 @@ struct TypesetBookImpl::Typesetter : BookTypesetter int spaceLeft = mPageHeight - (curPageStop - curPageStart); int sectionHeight = i->mRect.height (); - if (sectionHeight <= mPageHeight) + // This is NOT equal to i->mRect.height(), which doesn't account for section breaks. + int spaceRequired = (i->mRect.bottom - curPageStop); + if (curPageStart == curPageStop) // If this is a new page, the section break is not needed + spaceRequired = i->mRect.height(); + + if (spaceRequired <= mPageHeight) { - if (sectionHeight > spaceLeft) + if (spaceRequired > spaceLeft) { + // The section won't completely fit on the current page. Finish the current page and start a new one. assert (curPageStart != curPageStop); mBook->mPages.push_back (Page (curPageStart, curPageStop)); @@ -380,8 +386,6 @@ struct TypesetBookImpl::Typesetter : BookTypesetter int sectionHeightLeft = sectionHeight; while (sectionHeightLeft > mPageHeight) { - spaceLeft = mPageHeight - (curPageStop - curPageStart); - // Adjust to the top of the first line that does not fit on the current page anymore int splitPos = curPageStop; for (Lines::iterator j = i->mLines.begin (); j != i->mLines.end (); ++j) diff --git a/apps/openmw/mwgui/bookwindow.cpp b/apps/openmw/mwgui/bookwindow.cpp index 884d567c5..92e876708 100644 --- a/apps/openmw/mwgui/bookwindow.cpp +++ b/apps/openmw/mwgui/bookwindow.cpp @@ -70,11 +70,6 @@ namespace MWGui void BookWindow::clearPages() { - for (std::vector::iterator it=mPages.begin(); - it!=mPages.end(); ++it) - { - MyGUI::Gui::getInstance().destroyWidget(*it); - } mPages.clear(); } @@ -89,31 +84,23 @@ namespace MWGui MWWorld::LiveCellRef *ref = mBook.get(); - BookTextParser parser; - std::vector results = parser.split(ref->mBase->mText, mLeftPage->getSize().width, mLeftPage->getSize().height); - - int i=0; - for (std::vector::iterator it=results.begin(); - it!=results.end(); ++it) - { - MyGUI::Widget* parent; - if (i%2 == 0) - parent = mLeftPage; - else - parent = mRightPage; - - MyGUI::Widget* pageWidget = parent->createWidgetReal("", MyGUI::FloatCoord(0.0,0.0,1.0,1.0), MyGUI::Align::Default, "BookPage" + boost::lexical_cast(i)); - pageWidget->setNeedMouseFocus(false); - parser.parsePage(*it, pageWidget, mLeftPage->getSize().width); - mPages.push_back(pageWidget); - ++i; - } + Formatting::BookFormatter formatter; + mPages = formatter.markupToWidget(mLeftPage, ref->mBase->mText); + formatter.markupToWidget(mRightPage, ref->mBase->mText); updatePages(); setTakeButtonShow(true); } + void BookWindow::exit() + { + // no 3d sounds because the object could be in a container. + MWBase::Environment::get().getSoundManager()->playSound ("book close", 1.0, 1.0); + + MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Book); + } + void BookWindow::setTakeButtonShow(bool show) { mTakeButtonShow = show; @@ -128,10 +115,7 @@ namespace MWGui void BookWindow::onCloseButtonClicked (MyGUI::Widget* sender) { - // no 3d sounds because the object could be in a container. - MWBase::Environment::get().getSoundManager()->playSound ("book close", 1.0, 1.0); - - MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Book); + exit(); } void BookWindow::onTakeButtonClicked (MyGUI::Widget* sender) @@ -159,19 +143,6 @@ namespace MWGui mLeftPageNumber->setCaption( boost::lexical_cast(mCurrentPage*2 + 1) ); mRightPageNumber->setCaption( boost::lexical_cast(mCurrentPage*2 + 2) ); - unsigned int i=0; - for (std::vector::iterator it = mPages.begin(); - it != mPages.end(); ++it) - { - if (mCurrentPage*2 == i || mCurrentPage*2+1 == i) - (*it)->setVisible(true); - else - { - (*it)->setVisible(false); - } - ++i; - } - //If it is the last page, hide the button "Next Page" if ( (mCurrentPage+1)*2 == mPages.size() || (mCurrentPage+1)*2 == mPages.size() + 1) @@ -186,9 +157,30 @@ namespace MWGui } else { mPrevPageButton->setVisible(true); } + + if (mPages.empty()) + return; + + MyGUI::Widget * paper; + + paper = mLeftPage->getChildAt(0); + paper->setCoord(paper->getPosition().left, -mPages[mCurrentPage*2].first, + paper->getWidth(), mPages[mCurrentPage*2].second); + + paper = mRightPage->getChildAt(0); + if ((mCurrentPage+1)*2 <= mPages.size()) + { + paper->setCoord(paper->getPosition().left, -mPages[mCurrentPage*2+1].first, + paper->getWidth(), mPages[mCurrentPage*2+1].second); + paper->setVisible(true); + } + else + { + paper->setVisible(false); + } } - void BookWindow::adjustButton (MWGui::ImageButton* button) + void BookWindow::adjustButton (Gui::ImageButton* button) { MyGUI::IntSize diff = button->getSize() - button->getRequestedSize(); button->setSize(button->getRequestedSize()); diff --git a/apps/openmw/mwgui/bookwindow.hpp b/apps/openmw/mwgui/bookwindow.hpp index f8821ab50..ea3057a6f 100644 --- a/apps/openmw/mwgui/bookwindow.hpp +++ b/apps/openmw/mwgui/bookwindow.hpp @@ -5,7 +5,7 @@ #include "../mwworld/ptr.hpp" -#include "imagebutton.hpp" +#include namespace MWGui { @@ -14,6 +14,8 @@ namespace MWGui public: BookWindow(); + virtual void exit(); + void open(MWWorld::Ptr book); void setTakeButtonShow(bool show); void nextPage(); @@ -29,20 +31,24 @@ namespace MWGui void updatePages(); void clearPages(); - void adjustButton(MWGui::ImageButton* button); + void adjustButton(Gui::ImageButton* button); private: - MWGui::ImageButton* mCloseButton; - MWGui::ImageButton* mTakeButton; - MWGui::ImageButton* mNextPageButton; - MWGui::ImageButton* mPrevPageButton; + typedef std::pair Page; + typedef std::vector Pages; + + Gui::ImageButton* mCloseButton; + Gui::ImageButton* mTakeButton; + Gui::ImageButton* mNextPageButton; + Gui::ImageButton* mPrevPageButton; + MyGUI::TextBox* mLeftPageNumber; MyGUI::TextBox* mRightPageNumber; MyGUI::Widget* mLeftPage; MyGUI::Widget* mRightPage; unsigned int mCurrentPage; // 0 is first page - std::vector mPages; + Pages mPages; MWWorld::Ptr mBook; diff --git a/apps/openmw/mwgui/class.cpp b/apps/openmw/mwgui/class.cpp index 1c8cc7840..4e45f1a7c 100644 --- a/apps/openmw/mwgui/class.cpp +++ b/apps/openmw/mwgui/class.cpp @@ -17,9 +17,6 @@ namespace MWGui GenerateClassResultDialog::GenerateClassResultDialog() : WindowModal("openmw_chargen_generate_class_result.layout") { - // Centre dialog - center(); - setText("ReflectT", MWBase::Environment::get().getWindowManager()->getGameSettingString("sMessageQuestionAnswer1", "")); getWidget(mClassImage, "ClassImage"); @@ -34,6 +31,8 @@ namespace MWGui getWidget(okButton, "OKButton"); okButton->setCaptionWithReplacing("#{sMessageQuestionAnswer2}"); okButton->eventMouseButtonClick += MyGUI::newDelegate(this, &GenerateClassResultDialog::onOkClicked); + + center(); } std::string GenerateClassResultDialog::getClassId() const @@ -46,6 +45,8 @@ namespace MWGui mCurrentClassId = classId; mClassImage->setImageTexture(std::string("textures\\levelup\\") + mCurrentClassId + ".dds"); mClassName->setCaption(MWBase::Environment::get().getWorld()->getStore().get().find(mCurrentClassId)->mName); + + center(); } // widget controls @@ -276,7 +277,6 @@ namespace MWGui InfoBoxDialog::InfoBoxDialog() : WindowModal("openmw_infobox.layout") - , mCurrentButton(-1) { getWidget(mTextBox, "TextBox"); getWidget(mText, "Text"); @@ -305,7 +305,6 @@ namespace MWGui MyGUI::Gui::getInstance().destroyWidget(*it); } this->mButtons.clear(); - mCurrentButton = -1; // TODO: The buttons should be generated from a template in the layout file, ie. cloning an existing widget MyGUI::Button* button; @@ -335,11 +334,6 @@ namespace MWGui center(); } - int InfoBoxDialog::getChosenButton() const - { - return mCurrentButton; - } - void InfoBoxDialog::onButtonClicked(MyGUI::Widget* _sender) { std::vector::const_iterator end = mButtons.end(); @@ -348,7 +342,6 @@ namespace MWGui { if (*it == _sender) { - mCurrentButton = i; eventButtonSelected(i); return; } @@ -670,8 +663,6 @@ namespace MWGui // Centre dialog center(); - setText("LabelT", MWBase::Environment::get().getWindowManager()->getGameSettingString("sSpecializationMenu1", "")); - getWidget(mSpecialization0, "Specialization0"); getWidget(mSpecialization1, "Specialization1"); getWidget(mSpecialization2, "Specialization2"); @@ -693,7 +684,6 @@ namespace MWGui MyGUI::Button* cancelButton; getWidget(cancelButton, "CancelButton"); - cancelButton->setCaption(MWBase::Environment::get().getWindowManager()->getGameSettingString("sCancel", "")); cancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &SelectSpecializationDialog::onCancelClicked); } @@ -718,6 +708,11 @@ namespace MWGui } void SelectSpecializationDialog::onCancelClicked(MyGUI::Widget* _sender) + { + exit(); + } + + void SelectSpecializationDialog::exit() { eventCancel(); } @@ -731,8 +726,6 @@ namespace MWGui // Centre dialog center(); - setText("LabelT", MWBase::Environment::get().getWindowManager()->getGameSettingString("sAttributesMenu1", "")); - for (int i = 0; i < 8; ++i) { Widgets::MWAttributePtr attribute; @@ -746,7 +739,6 @@ namespace MWGui MyGUI::Button* cancelButton; getWidget(cancelButton, "CancelButton"); - cancelButton->setCaption(MWBase::Environment::get().getWindowManager()->getGameSettingString("sCancel", "")); cancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &SelectAttributeDialog::onCancelClicked); } @@ -764,6 +756,11 @@ namespace MWGui } void SelectAttributeDialog::onCancelClicked(MyGUI::Widget* _sender) + { + exit(); + } + + void SelectAttributeDialog::exit() { eventCancel(); } @@ -777,11 +774,6 @@ namespace MWGui // Centre dialog center(); - setText("LabelT", MWBase::Environment::get().getWindowManager()->getGameSettingString("sSkillsMenu1", "")); - setText("CombatLabelT", MWBase::Environment::get().getWindowManager()->getGameSettingString("sSpecializationCombat", "")); - setText("MagicLabelT", MWBase::Environment::get().getWindowManager()->getGameSettingString("sSpecializationMagic", "")); - setText("StealthLabelT", MWBase::Environment::get().getWindowManager()->getGameSettingString("sSpecializationStealth", "")); - for(int i = 0; i < 9; i++) { char theIndex = '0'+i; @@ -838,7 +830,6 @@ namespace MWGui MyGUI::Button* cancelButton; getWidget(cancelButton, "CancelButton"); - cancelButton->setCaption(MWBase::Environment::get().getWindowManager()->getGameSettingString("sCancel", "")); cancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &SelectSkillDialog::onCancelClicked); } @@ -855,6 +846,11 @@ namespace MWGui } void SelectSkillDialog::onCancelClicked(MyGUI::Widget* _sender) + { + exit(); + } + + void SelectSkillDialog::exit() { eventCancel(); } diff --git a/apps/openmw/mwgui/class.hpp b/apps/openmw/mwgui/class.hpp index f78f7541b..40056ca5e 100644 --- a/apps/openmw/mwgui/class.hpp +++ b/apps/openmw/mwgui/class.hpp @@ -24,7 +24,6 @@ namespace MWGui void setButtons(ButtonList &buttons); virtual void open(); - int getChosenButton() const; // Events typedef MyGUI::delegates::CMultiDelegate1 EventHandle_Int; @@ -41,7 +40,6 @@ namespace MWGui void fitToText(MyGUI::TextBox* widget); void layoutVertically(MyGUI::Widget* widget, int margin); - int mCurrentButton; MyGUI::Widget* mTextBox; MyGUI::TextBox* mText; MyGUI::Widget* mButtonBar; @@ -79,6 +77,11 @@ namespace MWGui */ EventHandle_Void eventBack; + /** Event : Dialog finished, OK button clicked.\n + signature : void method()\n + */ + EventHandle_WindowBase eventDone; + protected: void onOkClicked(MyGUI::Widget* _sender); void onBackClicked(MyGUI::Widget* _sender); @@ -109,6 +112,11 @@ namespace MWGui */ EventHandle_Void eventBack; + /** Event : Dialog finished, OK button clicked.\n + signature : void method()\n + */ + EventHandle_WindowBase eventDone; + protected: void onSelectClass(MyGUI::ListBox* _sender, size_t _index); void onAccept(MyGUI::ListBox* _sender, size_t _index); @@ -136,6 +144,8 @@ namespace MWGui SelectSpecializationDialog(); ~SelectSpecializationDialog(); + virtual void exit(); + ESM::Class::Specialization getSpecializationId() const { return mSpecializationId; } // Events @@ -167,6 +177,8 @@ namespace MWGui SelectAttributeDialog(); ~SelectAttributeDialog(); + virtual void exit(); + ESM::Attribute::AttributeID getAttributeId() const { return mAttributeId; } // Events @@ -196,6 +208,8 @@ namespace MWGui SelectSkillDialog(); ~SelectSkillDialog(); + virtual void exit(); + ESM::Skill::SkillEnum getSkillId() const { return mSkillId; } // Events @@ -232,6 +246,11 @@ namespace MWGui std::string getTextInput() const { return mTextEdit->getCaption(); } void setTextInput(const std::string &text) { mTextEdit->setCaption(text); } + /** Event : Dialog finished, OK button clicked.\n + signature : void method()\n + */ + EventHandle_WindowBase eventDone; + protected: void onOkClicked(MyGUI::Widget* _sender); @@ -262,6 +281,11 @@ namespace MWGui */ EventHandle_Void eventBack; + /** Event : Dialog finished, OK button clicked.\n + signature : void method()\n + */ + EventHandle_WindowBase eventDone; + protected: void onOkClicked(MyGUI::Widget* _sender); void onBackClicked(MyGUI::Widget* _sender); diff --git a/apps/openmw/mwgui/companionwindow.cpp b/apps/openmw/mwgui/companionwindow.cpp index 70093049d..2dd130b0d 100644 --- a/apps/openmw/mwgui/companionwindow.cpp +++ b/apps/openmw/mwgui/companionwindow.cpp @@ -50,6 +50,13 @@ void CompanionWindow::onItemSelected(int index) const ItemStack& item = mSortModel->getItem(index); + // We can't take conjured items from a companion NPC + if (item.mFlags & ItemStack::Flag_Bound) + { + MWBase::Environment::get().getWindowManager()->messageBox("#{sBarterDialog12}"); + return; + } + MWWorld::Ptr object = item.mBase; int count = item.mCount; bool shift = MyGUI::InputManager::getInstance().isShiftPressed(); @@ -118,6 +125,11 @@ void CompanionWindow::updateEncumbranceBar() } void CompanionWindow::onCloseButtonClicked(MyGUI::Widget* _sender) +{ + exit(); +} + +void CompanionWindow::exit() { if (mPtr.getTypeName() == typeid(ESM::NPC).name() && mPtr.getClass().getNpcStats(mPtr).getProfit() < 0) { @@ -136,10 +148,11 @@ void CompanionWindow::onMessageBoxButtonClicked(int button) if (button == 0) { mPtr.getRefData().getLocals().setVarByInt(mPtr.getClass().getScript(mPtr), - "minimumProfit", mPtr.getClass().getNpcStats(mPtr).getProfit()); + "minimumprofit", mPtr.getClass().getNpcStats(mPtr).getProfit()); MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Companion); - MWBase::Environment::get().getDialogueManager()->startDialogue (mPtr); + // Important for Calvus' contract script to work properly + MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Dialogue); } } @@ -148,6 +161,13 @@ void CompanionWindow::onReferenceUnavailable() MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Companion); } +void CompanionWindow::resetReference() +{ + ReferenceInterface::resetReference(); + mItemView->setModel(NULL); + mModel = NULL; + mSortModel = NULL; +} } diff --git a/apps/openmw/mwgui/companionwindow.hpp b/apps/openmw/mwgui/companionwindow.hpp index 02e3812f0..dc460e2fc 100644 --- a/apps/openmw/mwgui/companionwindow.hpp +++ b/apps/openmw/mwgui/companionwindow.hpp @@ -18,6 +18,10 @@ namespace MWGui public: CompanionWindow(DragAndDrop* dragAndDrop, MessageBoxManager* manager); + virtual void exit(); + + virtual void resetReference(); + void open(const MWWorld::Ptr& npc); void onFrame (); diff --git a/apps/openmw/mwgui/confirmationdialog.cpp b/apps/openmw/mwgui/confirmationdialog.cpp index f431f2f64..a4e10749a 100644 --- a/apps/openmw/mwgui/confirmationdialog.cpp +++ b/apps/openmw/mwgui/confirmationdialog.cpp @@ -13,12 +13,15 @@ namespace MWGui mOkButton->eventMouseButtonClick += MyGUI::newDelegate(this, &ConfirmationDialog::onOkButtonClicked); } - void ConfirmationDialog::open(const std::string& message) + void ConfirmationDialog::open(const std::string& message, const std::string& confirmMessage, const std::string& cancelMessage) { setVisible(true); mMessage->setCaptionWithReplacing(message); + mCancelButton->setCaptionWithReplacing(cancelMessage); + mOkButton->setCaptionWithReplacing(confirmMessage); + int height = mMessage->getTextSize().height + 72; mMainWidget->setSize(mMainWidget->getWidth(), height); @@ -28,17 +31,22 @@ namespace MWGui center(); } - void ConfirmationDialog::onCancelButtonClicked(MyGUI::Widget* _sender) + void ConfirmationDialog::exit() { + setVisible(false); + eventCancelClicked(); + } - setVisible(false); + void ConfirmationDialog::onCancelButtonClicked(MyGUI::Widget* _sender) + { + exit(); } void ConfirmationDialog::onOkButtonClicked(MyGUI::Widget* _sender) { - eventOkClicked(); - setVisible(false); + + eventOkClicked(); } } diff --git a/apps/openmw/mwgui/confirmationdialog.hpp b/apps/openmw/mwgui/confirmationdialog.hpp index 47b256017..7ff16b10a 100644 --- a/apps/openmw/mwgui/confirmationdialog.hpp +++ b/apps/openmw/mwgui/confirmationdialog.hpp @@ -9,7 +9,8 @@ namespace MWGui { public: ConfirmationDialog(); - void open(const std::string& message); + void open(const std::string& message, const std::string& confirmMessage="#{sOk}", const std::string& cancelMessage="#{sCancel}"); + virtual void exit(); typedef MyGUI::delegates::CMultiDelegate0 EventHandle_Void; diff --git a/apps/openmw/mwgui/console.cpp b/apps/openmw/mwgui/console.cpp index 811f93b48..5a7193d65 100644 --- a/apps/openmw/mwgui/console.cpp +++ b/apps/openmw/mwgui/console.cpp @@ -61,7 +61,7 @@ namespace MWGui } catch (const std::exception& error) { - printError (std::string ("An exception has been thrown: ") + error.what()); + printError (std::string ("Error: ") + error.what()); } return false; @@ -140,18 +140,19 @@ namespace MWGui void Console::close() { // Apparently, hidden widgets can retain key focus + // Remove for MyGUI 3.2.2 MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(NULL); } - void Console::setFont(const std::string &fntName) + void Console::exit() { - mHistory->setFontName(fntName); - mCommandLine->setFontName(fntName); + MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Console); } - void Console::clearHistory() + void Console::setFont(const std::string &fntName) { - mHistory->setCaption(""); + mHistory->setFontName(fntName); + mCommandLine->setFontName(fntName); } void Console::print(const std::string &msg) @@ -190,7 +191,7 @@ namespace MWGui } catch (const std::exception& error) { - printError (std::string ("An exception has been thrown: ") + error.what()); + printError (std::string ("Error: ") + error.what()); } } } @@ -430,4 +431,10 @@ namespace MWGui { setSelectedObject(MWWorld::Ptr()); } + + void Console::resetReference() + { + ReferenceInterface::resetReference(); + setSelectedObject(MWWorld::Ptr()); + } } diff --git a/apps/openmw/mwgui/console.hpp b/apps/openmw/mwgui/console.hpp index 890176363..09a05be48 100644 --- a/apps/openmw/mwgui/console.hpp +++ b/apps/openmw/mwgui/console.hpp @@ -21,86 +21,83 @@ namespace MWGui { - class Console : public WindowBase, private Compiler::ErrorHandler, public ReferenceInterface - { - private: + class Console : public WindowBase, private Compiler::ErrorHandler, public ReferenceInterface + { + public: + /// Set the implicit object for script execution + void setSelectedObject(const MWWorld::Ptr& object); - Compiler::Extensions mExtensions; - MWScript::CompilerContext mCompilerContext; - std::vector mNames; - bool mConsoleOnlyScripts; + MyGUI::EditBox* mCommandLine; + MyGUI::EditBox* mHistory; - bool compile (const std::string& cmd, Compiler::Output& output); + typedef std::list StringList; - /// Report error to the user. - virtual void report (const std::string& message, const Compiler::TokenLoc& loc, Type type); + // History of previous entered commands + StringList mCommandHistory; + StringList::iterator mCurrent; + std::string mEditString; - /// Report a file related error - virtual void report (const std::string& message, Type type); + Console(int w, int h, bool consoleOnlyScripts); - void listNames(); - ///< Write all valid identifiers and keywords into mNames and sort them. - /// \note If mNames is not empty, this function is a no-op. - /// \note The list may contain duplicates (if a name is a keyword and an identifier at the same - /// time). + virtual void open(); + virtual void close(); - public: + virtual void exit(); - void setSelectedObject(const MWWorld::Ptr& object); - ///< Set the implicit object for script execution + void setFont(const std::string &fntName); - protected: + void onResChange(int width, int height); - virtual void onReferenceUnavailable(); + // Print a message to the console. Messages may contain color + // code, eg. "#FFFFFF this is white". + void print(const std::string &msg); + // These are pre-colored versions that you should use. - public: - MyGUI::EditBox* mCommandLine; - MyGUI::EditBox* mHistory; + /// Output from successful console command + void printOK(const std::string &msg); - typedef std::list StringList; + /// Error message + void printError(const std::string &msg); - // History of previous entered commands - StringList mCommandHistory; - StringList::iterator mCurrent; - std::string mEditString; + void execute (const std::string& command); - Console(int w, int h, bool consoleOnlyScripts); + void executeFile (const std::string& path); - virtual void open(); - virtual void close(); + virtual void resetReference (); - void setFont(const std::string &fntName); + protected: - void onResChange(int width, int height); + virtual void onReferenceUnavailable(); - void clearHistory(); + private: - // Print a message to the console. Messages may contain color - // code, eg. "#FFFFFF this is white". - void print(const std::string &msg); + void keyPress(MyGUI::Widget* _sender, + MyGUI::KeyCode key, + MyGUI::Char _char); - // These are pre-colored versions that you should use. + void acceptCommand(MyGUI::EditBox* _sender); - /// Output from successful console command - void printOK(const std::string &msg); + std::string complete( std::string input, std::vector &matches ); - /// Error message - void printError(const std::string &msg); + Compiler::Extensions mExtensions; + MWScript::CompilerContext mCompilerContext; + std::vector mNames; + bool mConsoleOnlyScripts; - void execute (const std::string& command); + bool compile (const std::string& cmd, Compiler::Output& output); - void executeFile (const std::string& path); + /// Report error to the user. + virtual void report (const std::string& message, const Compiler::TokenLoc& loc, Type type); - private: + /// Report a file related error + virtual void report (const std::string& message, Type type); - void keyPress(MyGUI::Widget* _sender, - MyGUI::KeyCode key, - MyGUI::Char _char); - - void acceptCommand(MyGUI::EditBox* _sender); - - std::string complete( std::string input, std::vector &matches ); + /// Write all valid identifiers and keywords into mNames and sort them. + /// \note If mNames is not empty, this function is a no-op. + /// \note The list may contain duplicates (if a name is a keyword and an identifier at the same + /// time). + void listNames(); }; } #endif diff --git a/apps/openmw/mwgui/container.cpp b/apps/openmw/mwgui/container.cpp index a207dec0c..58f23c175 100644 --- a/apps/openmw/mwgui/container.cpp +++ b/apps/openmw/mwgui/container.cpp @@ -20,6 +20,7 @@ #include "inventorywindow.hpp" #include "itemview.hpp" +#include "itemwidget.hpp" #include "inventoryitemmodel.hpp" #include "sortfilteritemmodel.hpp" #include "pickpocketitemmodel.hpp" @@ -27,6 +28,16 @@ namespace MWGui { + DragAndDrop::DragAndDrop() + : mDraggedWidget(NULL) + , mDraggedCount(0) + , mSourceModel(NULL) + , mSourceView(NULL) + , mSourceSortModel(NULL) + , mIsOnDragAndDrop(false) + { + } + void DragAndDrop::startDrag (int index, SortFilterItemModel* sortModel, ItemModel* sourceModel, ItemView* sourceView, int count) { mItem = sourceModel->getItem(index); @@ -35,7 +46,32 @@ namespace MWGui mSourceView = sourceView; mSourceSortModel = sortModel; mIsOnDragAndDrop = true; - mDragAndDropWidget->setVisible(true); + + // If picking up an item that isn't from the player's inventory, the item gets added to player inventory backend + // immediately, even though it's still floating beneath the mouse cursor. A bit counterintuitive, + // but this is how it works in vanilla, and not doing so would break quests (BM_beasts for instance). + ItemModel* playerModel = MWBase::Environment::get().getWindowManager()->getInventoryWindow()->getModel(); + if (mSourceModel != playerModel) + { + MWWorld::Ptr item = mSourceModel->moveItem(mItem, mDraggedCount, playerModel); + + playerModel->update(); + + ItemModel::ModelIndex newIndex = -1; + for (unsigned int i=0; igetItemCount(); ++i) + { + if (playerModel->getItem(i).mBase == item) + { + newIndex = i; + break; + } + } + mItem = playerModel->getItem(newIndex); + mSourceModel = playerModel; + + SortFilterItemModel* playerFilterModel = MWBase::Environment::get().getWindowManager()->getInventoryWindow()->getSortFilterModel(); + mSourceSortModel = playerFilterModel; + } std::string sound = mItem.mBase.getClass().getUpSoundId(mItem.mBase); MWBase::Environment::get().getSoundManager()->playSound (sound, 1.0, 1.0); @@ -46,28 +82,17 @@ namespace MWGui mSourceSortModel->addDragItem(mItem.mBase, count); } - std::string path = std::string("icons\\"); - path += mItem.mBase.getClass().getInventoryIcon(mItem.mBase); - MyGUI::ImageBox* baseWidget = mDragAndDropWidget->createWidget - ("ImageBox", MyGUI::IntCoord(0, 0, 42, 42), MyGUI::Align::Default); + ItemWidget* baseWidget = MyGUI::Gui::getInstance().createWidget("MW_ItemIcon", 0, 0, 42, 42, MyGUI::Align::Default, "DragAndDrop"); + + Controllers::ControllerFollowMouse* controller = + MyGUI::ControllerManager::getInstance().createItem(Controllers::ControllerFollowMouse::getClassTypeName()) + ->castType(); + MyGUI::ControllerManager::getInstance().addItem(baseWidget, controller); + mDraggedWidget = baseWidget; - MyGUI::ImageBox* image = baseWidget->createWidget("ImageBox", - MyGUI::IntCoord(5, 5, 32, 32), MyGUI::Align::Default); - size_t pos = path.rfind("."); - if (pos != std::string::npos) - path.erase(pos); - path.append(".dds"); - image->setImageTexture(path); - image->setNeedMouseFocus(false); - - // text widget that shows item count - MyGUI::TextBox* text = image->createWidget("SandBrightText", - MyGUI::IntCoord(0, 14, 32, 18), MyGUI::Align::Default, std::string("Label")); - text->setTextAlign(MyGUI::Align::Right); - text->setNeedMouseFocus(false); - text->setTextShadow(true); - text->setTextShadowColour(MyGUI::Colour(0,0,0)); - text->setCaption(ItemView::getCountString(count)); + baseWidget->setItem(mItem.mBase); + baseWidget->setNeedMouseFocus(false); + baseWidget->setCount(count); sourceView->update(); @@ -79,7 +104,12 @@ namespace MWGui std::string sound = mItem.mBase.getClass().getDownSoundId(mItem.mBase); MWBase::Environment::get().getSoundManager()->playSound (sound, 1.0, 1.0); - mDragAndDropWidget->setVisible(false); + // We can't drop a conjured item to the ground; the target container should always be the source container + if (mItem.mFlags & ItemStack::Flag_Bound && targetModel != mSourceModel) + { + MWBase::Environment::get().getWindowManager()->messageBox("#{sBarterDialog12}"); + return; + } // If item is dropped where it was taken from, we don't need to do anything - // otherwise, do the transfer @@ -144,6 +174,13 @@ namespace MWGui const ItemStack& item = mSortModel->getItem(index); + // We can't take a conjured item from a container (some NPC we're pickpocketing, a box, etc) + if (item.mFlags & ItemStack::Flag_Bound) + { + MWBase::Environment::get().getWindowManager()->messageBox("#{sContentsMessage1}"); + return; + } + MWWorld::Ptr object = item.mBase; int count = item.mCount; bool shift = MyGUI::InputManager::getInstance().isShiftPressed(); @@ -175,15 +212,6 @@ namespace MWGui { if (mPtr.getTypeName() == typeid(ESM::Container).name()) { - // check that we don't exceed container capacity - MWWorld::Ptr item = mDragAndDrop->mItem.mBase; - float weight = item.getClass().getWeight(item) * mDragAndDrop->mDraggedCount; - if (mPtr.getClass().getCapacity(mPtr) < mPtr.getClass().getEncumbrance(mPtr) + weight) - { - MWBase::Environment::get().getWindowManager()->messageBox("#{sContentsMessage3}"); - return; - } - // check container organic flag MWWorld::LiveCellRef* ref = mPtr.get(); if (ref->mBase->mFlags & ESM::Container::Organic) @@ -192,6 +220,15 @@ namespace MWGui messageBox("#{sContentsMessage2}"); return; } + + // check that we don't exceed container capacity + MWWorld::Ptr item = mDragAndDrop->mItem.mBase; + float weight = item.getClass().getWeight(item) * mDragAndDrop->mDraggedCount; + if (mPtr.getClass().getCapacity(mPtr) < mPtr.getClass().getEncumbrance(mPtr) + weight) + { + MWBase::Environment::get().getWindowManager()->messageBox("#{sContentsMessage3}"); + return; + } } mDragAndDrop->drop(mModel, mItemView); @@ -212,7 +249,8 @@ namespace MWGui { // we are stealing stuff MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); - mModel = new PickpocketItemModel(player, new InventoryItemModel(container)); + mModel = new PickpocketItemModel(player, new InventoryItemModel(container), + !mPtr.getClass().getCreatureStats(mPtr).getKnockedDown()); } else mModel = new InventoryItemModel(container); @@ -238,6 +276,14 @@ namespace MWGui onTakeAllButtonClicked(mTakeButton); } + void ContainerWindow::resetReference() + { + ReferenceInterface::resetReference(); + mItemView->setModel(NULL); + mModel = NULL; + mSortModel = NULL; + } + void ContainerWindow::close() { WindowBase::close(); @@ -263,7 +309,7 @@ namespace MWGui } } - void ContainerWindow::onCloseButtonClicked(MyGUI::Widget* _sender) + void ContainerWindow::exit() { if(mDragAndDrop == NULL || !mDragAndDrop->mIsOnDragAndDrop) { @@ -271,6 +317,11 @@ namespace MWGui } } + void ContainerWindow::onCloseButtonClicked(MyGUI::Widget* _sender) + { + exit(); + } + void ContainerWindow::onTakeAllButtonClicked(MyGUI::Widget* _sender) { if(mDragAndDrop == NULL || !mDragAndDrop->mIsOnDragAndDrop) @@ -323,7 +374,9 @@ namespace MWGui bool ContainerWindow::onTakeItem(const ItemStack &item, int count) { MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); - if (dynamic_cast(mModel)) + // TODO: move to ItemModels + if (dynamic_cast(mModel) + && !mPtr.getClass().getCreatureStats(mPtr).getKnockedDown()) { MWMechanics::Pickpocket pickpocket(player, mPtr); if (pickpocket.pick(item.mBase, count)) diff --git a/apps/openmw/mwgui/container.hpp b/apps/openmw/mwgui/container.hpp index 3aa923a23..e32c6efaf 100644 --- a/apps/openmw/mwgui/container.hpp +++ b/apps/openmw/mwgui/container.hpp @@ -33,13 +33,14 @@ namespace MWGui public: bool mIsOnDragAndDrop; MyGUI::Widget* mDraggedWidget; - MyGUI::Widget* mDragAndDropWidget; ItemModel* mSourceModel; ItemView* mSourceView; SortFilterItemModel* mSourceSortModel; ItemStack mItem; int mDraggedCount; + DragAndDrop(); + void startDrag (int index, SortFilterItemModel* sortModel, ItemModel* sourceModel, ItemView* sourceView, int count); void drop (ItemModel* targetModel, ItemView* targetView); @@ -54,6 +55,10 @@ namespace MWGui void open(const MWWorld::Ptr& container, bool loot=false); virtual void close(); + virtual void resetReference(); + + virtual void exit(); + private: DragAndDrop* mDragAndDrop; diff --git a/apps/openmw/mwgui/controllers.cpp b/apps/openmw/mwgui/controllers.cpp index e62fb3fce..5ebd2b1e7 100644 --- a/apps/openmw/mwgui/controllers.cpp +++ b/apps/openmw/mwgui/controllers.cpp @@ -1,11 +1,13 @@ #include "controllers.hpp" +#include + namespace MWGui { namespace Controllers { - ControllerRepeatClick::ControllerRepeatClick() : + ControllerRepeatEvent::ControllerRepeatEvent() : mInit(0.5), mStep(0.1), mEnabled(true), @@ -13,11 +15,11 @@ namespace MWGui { } - ControllerRepeatClick::~ControllerRepeatClick() + ControllerRepeatEvent::~ControllerRepeatEvent() { } - bool ControllerRepeatClick::addTime(MyGUI::Widget* _widget, float _time) + bool ControllerRepeatEvent::addTime(MyGUI::Widget* _widget, float _time) { if(mTimeLeft == 0) mTimeLeft = mInit; @@ -31,24 +33,36 @@ namespace MWGui return true; } - void ControllerRepeatClick::setRepeat(float init, float step) + void ControllerRepeatEvent::setRepeat(float init, float step) { mInit = init; mStep = step; } - void ControllerRepeatClick::setEnabled(bool enable) + void ControllerRepeatEvent::setEnabled(bool enable) { mEnabled = enable; } - void ControllerRepeatClick::setProperty(const std::string& _key, const std::string& _value) + void ControllerRepeatEvent::setProperty(const std::string& _key, const std::string& _value) { } - void ControllerRepeatClick::prepareItem(MyGUI::Widget* _widget) + void ControllerRepeatEvent::prepareItem(MyGUI::Widget* _widget) + { + } + + // ------------------------------------------------------------- + + void ControllerFollowMouse::prepareItem(MyGUI::Widget *_widget) { } + bool ControllerFollowMouse::addTime(MyGUI::Widget *_widget, float _time) + { + _widget->setPosition(MyGUI::InputManager::getInstance().getMousePosition()); + return true; + } + } } diff --git a/apps/openmw/mwgui/controllers.hpp b/apps/openmw/mwgui/controllers.hpp index 798acde62..003027a46 100644 --- a/apps/openmw/mwgui/controllers.hpp +++ b/apps/openmw/mwgui/controllers.hpp @@ -9,14 +9,15 @@ namespace MWGui { namespace Controllers { - class ControllerRepeatClick : + // Should be removed when upgrading to MyGUI 3.2.2 (current git), it has ControllerRepeatClick + class ControllerRepeatEvent : public MyGUI::ControllerItem { - MYGUI_RTTI_DERIVED( ControllerRepeatClick ) + MYGUI_RTTI_DERIVED( ControllerRepeatEvent ) public: - ControllerRepeatClick(); - virtual ~ControllerRepeatClick(); + ControllerRepeatEvent(); + virtual ~ControllerRepeatEvent(); void setRepeat(float init, float step); void setEnabled(bool enable); @@ -40,6 +41,17 @@ namespace MWGui bool mEnabled; float mTimeLeft; }; + + /// Automatically positions a widget below the mouse cursor. + class ControllerFollowMouse : + public MyGUI::ControllerItem + { + MYGUI_RTTI_DERIVED( ControllerFollowMouse ) + + private: + bool addTime(MyGUI::Widget* _widget, float _time); + void prepareItem(MyGUI::Widget* _widget); + }; } } diff --git a/apps/openmw/mwgui/countdialog.cpp b/apps/openmw/mwgui/countdialog.cpp index 02ccbbc05..24d0af0d7 100644 --- a/apps/openmw/mwgui/countdialog.cpp +++ b/apps/openmw/mwgui/countdialog.cpp @@ -2,6 +2,8 @@ #include +#include + #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" @@ -19,7 +21,7 @@ namespace MWGui mCancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &CountDialog::onCancelButtonClicked); mOkButton->eventMouseButtonClick += MyGUI::newDelegate(this, &CountDialog::onOkButtonClicked); - mItemEdit->eventEditTextChange += MyGUI::newDelegate(this, &CountDialog::onEditTextChange); + mItemEdit->eventValueChanged += MyGUI::newDelegate(this, &CountDialog::onEditValueChanged); mSlider->eventScrollChangePosition += MyGUI::newDelegate(this, &CountDialog::onSliderMoved); // make sure we read the enter key being pressed to accept multiple items mItemEdit->eventEditSelectAccept += MyGUI::newDelegate(this, &CountDialog::onEnterKeyPressed); @@ -46,10 +48,18 @@ namespace MWGui MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mItemEdit); mSlider->setScrollPosition(maxCount-1); - mItemEdit->setCaption(boost::lexical_cast(maxCount)); + + mItemEdit->setMinValue(1); + mItemEdit->setMaxValue(maxCount); + mItemEdit->setValue(maxCount); + } + + void CountDialog::cancel() //Keeping this here as I don't know if anything else relies on it. + { + exit(); } - void CountDialog::cancel() + void CountDialog::exit() { setVisible(false); } @@ -65,40 +75,23 @@ namespace MWGui setVisible(false); } - + // essentially duplicating what the OK button does if user presses // Enter key void CountDialog::onEnterKeyPressed(MyGUI::EditBox* _sender) { eventOkClicked(NULL, mSlider->getScrollPosition()+1); - + setVisible(false); } - - void CountDialog::onEditTextChange(MyGUI::EditBox* _sender) + + void CountDialog::onEditValueChanged(int value) { - if (_sender->getCaption() == "") - return; - - unsigned int count; - try - { - count = boost::lexical_cast(_sender->getCaption()); - } - catch (std::bad_cast&) - { - count = 1; - } - if (count > mSlider->getScrollRange()) - { - count = mSlider->getScrollRange(); - } - mSlider->setScrollPosition(count-1); - onSliderMoved(mSlider, count-1); + mSlider->setScrollPosition(value-1); } void CountDialog::onSliderMoved(MyGUI::ScrollBar* _sender, size_t _position) { - mItemEdit->setCaption(boost::lexical_cast(_position+1)); + mItemEdit->setValue(_position+1); } } diff --git a/apps/openmw/mwgui/countdialog.hpp b/apps/openmw/mwgui/countdialog.hpp index 06de3eb88..a54e99cf4 100644 --- a/apps/openmw/mwgui/countdialog.hpp +++ b/apps/openmw/mwgui/countdialog.hpp @@ -3,6 +3,11 @@ #include "windowbase.hpp" +namespace Gui +{ + class NumericEditBox; +} + namespace MWGui { class CountDialog : public WindowModal @@ -11,6 +16,7 @@ namespace MWGui CountDialog(); void open(const std::string& item, const std::string& message, const int maxCount); void cancel(); + virtual void exit(); typedef MyGUI::delegates::CMultiDelegate2 EventHandle_WidgetInt; @@ -21,7 +27,7 @@ namespace MWGui private: MyGUI::ScrollBar* mSlider; - MyGUI::EditBox* mItemEdit; + Gui::NumericEditBox* mItemEdit; MyGUI::TextBox* mItemText; MyGUI::TextBox* mLabelText; MyGUI::Button* mOkButton; @@ -29,7 +35,7 @@ namespace MWGui void onCancelButtonClicked(MyGUI::Widget* _sender); void onOkButtonClicked(MyGUI::Widget* _sender); - void onEditTextChange(MyGUI::EditBox* _sender); + void onEditValueChanged(int value); void onSliderMoved(MyGUI::ScrollBar* _sender, size_t _position); void onEnterKeyPressed(MyGUI::EditBox* _sender); }; diff --git a/apps/openmw/mwgui/debugwindow.cpp b/apps/openmw/mwgui/debugwindow.cpp new file mode 100644 index 000000000..fab386bda --- /dev/null +++ b/apps/openmw/mwgui/debugwindow.cpp @@ -0,0 +1,116 @@ +#include "debugwindow.hpp" + + +#include + +namespace +{ + void bulletDumpRecursive(CProfileIterator* pit, int spacing, std::stringstream& os) + { + pit->First(); + if (pit->Is_Done()) + return; + + float accumulated_time=0,parent_time = pit->Is_Root() ? CProfileManager::Get_Time_Since_Reset() : pit->Get_Current_Parent_Total_Time(); + int i,j; + int frames_since_reset = CProfileManager::Get_Frame_Count_Since_Reset(); + for (i=0;iGet_Current_Parent_Name())+" (total running time: "+MyGUI::utility::toString(parent_time,3)+" ms) ---\n"; + os << s; + //float totalTime = 0.f; + + int numChildren = 0; + + for (i = 0; !pit->Is_Done(); i++,pit->Next()) + { + numChildren++; + float current_total_time = pit->Get_Current_Total_Time(); + accumulated_time += current_total_time; + float fraction = parent_time > SIMD_EPSILON ? (current_total_time / parent_time) * 100 : 0.f; + + for (j=0;jGet_Current_Name()+" ("+MyGUI::utility::toString(fraction,2)+" %) :: "+MyGUI::utility::toString(ms,3)+" ms / frame ("+MyGUI::utility::toString(pit->Get_Current_Total_Calls())+" calls)\n"; + os << s; + //totalTime += current_total_time; + //recurse into children + } + + if (parent_time < accumulated_time) + { + os << "what's wrong\n"; + } + for (i=0;i SIMD_EPSILON ? ((parent_time - accumulated_time) / parent_time) * 100 : 0.f; + s = "Unaccounted: ("+MyGUI::utility::toString(unaccounted,3)+" %) :: "+MyGUI::utility::toString(parent_time - accumulated_time,3)+" ms\n"; + os << s; + + for (i=0;iEnter_Child(i); + bulletDumpRecursive(pit, spacing+3, os); + pit->Enter_Parent(); + } + } + + void bulletDumpAll(std::stringstream& os) + { + CProfileIterator* profileIterator = 0; + profileIterator = CProfileManager::Get_Iterator(); + + bulletDumpRecursive(profileIterator, 0, os); + + CProfileManager::Release_Iterator(profileIterator); + } +} + + +namespace MWGui +{ + + DebugWindow::DebugWindow() + : WindowBase("openmw_debug_window.layout") + { + getWidget(mTabControl, "TabControl"); + + // Ideas for other tabs: + // - Texture / compositor texture viewer + // - Log viewer + // - Material editor + // - Shader editor + + MyGUI::TabItem* item = mTabControl->addItem("Physics Profiler"); + mBulletProfilerEdit = item->createWidgetReal + ("LogEdit", MyGUI::FloatCoord(0,0,1,1), MyGUI::Align::Stretch); + + MyGUI::IntSize viewSize = MyGUI::RenderManager::getInstance().getViewSize(); + mMainWidget->setSize(viewSize); + } + + void DebugWindow::onFrame(float dt) + { + if (!isVisible()) + return; + + static float timer = 0; + timer -= dt; + + if (timer > 0) + return; + timer = 1; + + std::stringstream stream; + bulletDumpAll(stream); + + if (mBulletProfilerEdit->isTextSelection()) // pause updating while user is trying to copy text + return; + + size_t previousPos = mBulletProfilerEdit->getVScrollPosition(); + mBulletProfilerEdit->setCaption(stream.str()); + mBulletProfilerEdit->setVScrollPosition(std::min(previousPos, mBulletProfilerEdit->getVScrollRange()-1)); + } + +} diff --git a/apps/openmw/mwgui/debugwindow.hpp b/apps/openmw/mwgui/debugwindow.hpp new file mode 100644 index 000000000..af5b914ea --- /dev/null +++ b/apps/openmw/mwgui/debugwindow.hpp @@ -0,0 +1,24 @@ +#ifndef OPENMW_MWGUI_DEBUGWINDOW_H +#define OPENMW_MWGUI_DEBUGWINDOW_H + +#include "windowbase.hpp" + +namespace MWGui +{ + + class DebugWindow : public WindowBase + { + public: + DebugWindow(); + + void onFrame(float dt); + + private: + MyGUI::TabControl* mTabControl; + + MyGUI::EditBox* mBulletProfilerEdit; + }; + +} + +#endif diff --git a/apps/openmw/mwgui/dialogue.cpp b/apps/openmw/mwgui/dialogue.cpp index 000275794..6526b200f 100644 --- a/apps/openmw/mwgui/dialogue.cpp +++ b/apps/openmw/mwgui/dialogue.cpp @@ -3,6 +3,8 @@ #include #include +#include + #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/mechanicsmanager.hpp" @@ -17,7 +19,6 @@ #include "../mwdialogue/dialoguemanagerimp.hpp" #include "widgets.hpp" -#include "list.hpp" #include "tradewindow.hpp" #include "spellbuyingwindow.hpp" #include "travelwindow.hpp" @@ -25,6 +26,16 @@ #include "journalbooks.hpp" // to_utf8_span +namespace +{ + + MyGUI::Colour getTextColour (const std::string& type) + { + return MyGUI::Colour::parse(MyGUI::LanguageManager::getInstance().replaceTags("#{fontcolour=" + type + "}")); + } + +} + namespace MWGui { @@ -51,7 +62,7 @@ namespace MWGui void PersuasionDialog::onCancel(MyGUI::Widget *sender) { - setVisible(false); + exit(); } void PersuasionDialog::onPersuade(MyGUI::Widget *sender) @@ -87,6 +98,11 @@ namespace MWGui mGoldLabel->setCaptionWithReplacing("#{sGold}: " + boost::lexical_cast(playerGold)); } + void PersuasionDialog::exit() + { + setVisible(false); + } + // -------------------------------------------------------------------------------------------------- Response::Response(const std::string &text, const std::string &title) @@ -97,7 +113,7 @@ namespace MWGui void Response::write(BookTypesetter::Ptr typesetter, KeywordSearchT* keywordSearch, std::map& topicLinks) const { - BookTypesetter::Style* title = typesetter->createStyle("", MyGUI::Colour(223/255.f, 201/255.f, 159/255.f)); + BookTypesetter::Style* title = typesetter->createStyle("", getTextColour("header")); typesetter->sectionBreak(9); if (mTitle != "") typesetter->write(title, to_utf8_span(mTitle.c_str())); @@ -109,10 +125,10 @@ namespace MWGui // We need this copy for when @# hyperlinks are replaced std::string text = mText; - size_t pos_begin, pos_end; + size_t pos_end; for(;;) { - pos_begin = text.find('@'); + size_t pos_begin = text.find('@'); if (pos_begin != std::string::npos) pos_end = text.find('#', pos_begin); @@ -141,14 +157,14 @@ namespace MWGui if (hyperLinks.size() && MWBase::Environment::get().getWindowManager()->getTranslationDataStorage().hasTranslation()) { - BookTypesetter::Style* style = typesetter->createStyle("", MyGUI::Colour(202/255.f, 165/255.f, 96/255.f)); + BookTypesetter::Style* style = typesetter->createStyle("", getTextColour("normal")); size_t formatted = 0; // points to the first character that is not laid out yet for (std::map::iterator it = hyperLinks.begin(); it != hyperLinks.end(); ++it) { intptr_t topicId = it->second; - const MyGUI::Colour linkHot (143/255.f, 155/255.f, 218/255.f); - const MyGUI::Colour linkNormal (112/255.f, 126/255.f, 207/255.f); - const MyGUI::Colour linkActive (175/255.f, 184/255.f, 228/255.f); + const MyGUI::Colour linkHot (getTextColour("link_over")); + const MyGUI::Colour linkNormal (getTextColour("link")); + const MyGUI::Colour linkActive (getTextColour("link_pressed")); BookTypesetter::Style* hotStyle = typesetter->createHotStyle (style, linkNormal, linkHot, linkActive, topicId); if (formatted < it->first.first) typesetter->write(style, formatted, it->first.first); @@ -180,11 +196,11 @@ namespace MWGui void Response::addTopicLink(BookTypesetter::Ptr typesetter, intptr_t topicId, size_t begin, size_t end) const { - BookTypesetter::Style* style = typesetter->createStyle("", MyGUI::Colour(202/255.f, 165/255.f, 96/255.f)); + BookTypesetter::Style* style = typesetter->createStyle("", getTextColour("normal")); - const MyGUI::Colour linkHot (143/255.f, 155/255.f, 218/255.f); - const MyGUI::Colour linkNormal (112/255.f, 126/255.f, 207/255.f); - const MyGUI::Colour linkActive (175/255.f, 184/255.f, 228/255.f); + const MyGUI::Colour linkHot (getTextColour("link_over")); + const MyGUI::Colour linkNormal (getTextColour("link")); + const MyGUI::Colour linkActive (getTextColour("link_pressed")); if (topicId) style = typesetter->createHotStyle (style, linkNormal, linkHot, linkActive, topicId); @@ -198,7 +214,7 @@ namespace MWGui void Message::write(BookTypesetter::Ptr typesetter, KeywordSearchT* keywordSearch, std::map& topicLinks) const { - BookTypesetter::Style* title = typesetter->createStyle("", MyGUI::Colour(223/255.f, 201/255.f, 159/255.f)); + BookTypesetter::Style* title = typesetter->createStyle("", getTextColour("notify")); typesetter->sectionBreak(9); typesetter->write(title, to_utf8_span(mText.c_str())); } @@ -261,7 +277,19 @@ namespace MWGui BookPage::ClickCallback callback = boost::bind (&DialogueWindow::notifyLinkClicked, this, _1); mHistory->adviseLinkClicked(callback); - static_cast(mMainWidget)->eventWindowChangeCoord += MyGUI::newDelegate(this, &DialogueWindow::onWindowResize); + mMainWidget->castType()->eventWindowChangeCoord += MyGUI::newDelegate(this, &DialogueWindow::onWindowResize); + } + + void DialogueWindow::exit() + { + if ((!mEnabled || MWBase::Environment::get().getDialogueManager()->isInChoice()) + && !mGoodbye) + { + // in choice, not allowed to escape, but give access to main menu to allow loading other saves + MWBase::Environment::get().getWindowManager()->pushGuiMode (MWGui::GM_MainMenu); + } + else + MWBase::Environment::get().getDialogueManager()->goodbyeSelected(); } void DialogueWindow::onWindowResize(MyGUI::Window* _sender) @@ -281,9 +309,7 @@ namespace MWGui void DialogueWindow::onByeClicked(MyGUI::Widget* _sender) { - if (!mEnabled || MWBase::Environment::get().getDialogueManager()->isInChoice()) - return; - MWBase::Environment::get().getDialogueManager()->goodbyeSelected(); + exit(); } void DialogueWindow::onSelectTopic(const std::string& topic, int id) @@ -355,10 +381,11 @@ namespace MWGui } } - void DialogueWindow::startDialogue(MWWorld::Ptr actor, std::string npcName) + void DialogueWindow::startDialogue(MWWorld::Ptr actor, std::string npcName, bool resetHistory) { mGoodbye = false; mEnabled = true; + bool sameActor = (mPtr == actor); mPtr = actor; mTopicsList->setEnabled(true); setTitle(npcName); @@ -367,15 +394,43 @@ namespace MWGui mTopicsList->clear(); - for (std::vector::iterator it = mHistoryContents.begin(); it != mHistoryContents.end(); ++it) - delete (*it); - mHistoryContents.clear(); + if (resetHistory || !sameActor) + { + for (std::vector::iterator it = mHistoryContents.begin(); it != mHistoryContents.end(); ++it) + delete (*it); + mHistoryContents.clear(); + } for (std::vector::iterator it = mLinks.begin(); it != mLinks.end(); ++it) delete (*it); mLinks.clear(); updateOptions(); + + restock(); + } + + void DialogueWindow::restock() + { + MWMechanics::CreatureStats &sellerStats = mPtr.getClass().getCreatureStats(mPtr); + float delay = MWBase::Environment::get().getWorld()->getStore().get().find("fBarterGoldResetDelay")->getFloat(); + + if (MWBase::Environment::get().getWorld()->getTimeStamp() >= sellerStats.getLastRestockTime() + delay) + { + sellerStats.setGoldPool(mPtr.getClass().getBaseGold(mPtr)); + + mPtr.getClass().restock(mPtr); + + // Also restock any containers owned by this merchant, which are also available to buy in the trade window + std::vector itemSources; + MWBase::Environment::get().getWorld()->getContainersOwnedBy(mPtr, itemSources); + for (std::vector::iterator it = itemSources.begin(); it != itemSources.end(); ++it) + { + it->getClass().restock(*it); + } + + sellerStats.setLastRestockTime(MWBase::Environment::get().getWorld()->getTimeStamp()); + } } void DialogueWindow::setKeywords(std::list keyWords) @@ -452,7 +507,7 @@ namespace MWGui mScrollBar->setVisible(true); } - BookTypesetter::Ptr typesetter = BookTypesetter::create (mHistory->getWidth(), std::numeric_limits().max()); + BookTypesetter::Ptr typesetter = BookTypesetter::create (mHistory->getWidth(), std::numeric_limits::max()); for (std::vector::iterator it = mHistoryContents.begin(); it != mHistoryContents.end(); ++it) (*it)->write(typesetter, &mKeywordSearch, mTopicLinks); @@ -462,10 +517,10 @@ namespace MWGui typesetter->sectionBreak(9); // choices - const MyGUI::Colour linkHot (223/255.f, 201/255.f, 159/255.f); - const MyGUI::Colour linkNormal (150/255.f, 50/255.f, 30/255.f); - const MyGUI::Colour linkActive (243/255.f, 237/255.f, 221/255.f); - for (std::map::reverse_iterator it = mChoices.rbegin(); it != mChoices.rend(); ++it) + const MyGUI::Colour linkHot (getTextColour("answer_over")); + const MyGUI::Colour linkNormal (getTextColour("answer")); + const MyGUI::Colour linkActive (getTextColour("answer_pressed")); + for (std::vector >::iterator it = mChoices.begin(); it != mChoices.end(); ++it) { Choice* link = new Choice(it->second); mLinks.push_back(link); @@ -504,6 +559,14 @@ namespace MWGui // no scrollbar onScrollbarMoved(mScrollBar, 0); } + + MyGUI::Button* byeButton; + getWidget(byeButton, "ByeButton"); + bool goodbyeEnabled = !MWBase::Environment::get().getDialogueManager()->isInChoice() || mGoodbye; + byeButton->setEnabled(goodbyeEnabled); + + bool topicsEnabled = !MWBase::Environment::get().getDialogueManager()->isInChoice() && !mGoodbye; + mTopicsList->setEnabled(topicsEnabled); } void DialogueWindow::notifyLinkClicked (TypesetBook::InteractiveId link) @@ -547,7 +610,7 @@ namespace MWGui void DialogueWindow::addChoice(const std::string& choice, int id) { - mChoices[choice] = id; + mChoices.push_back(std::make_pair(choice, id)); updateHistory(); } @@ -562,12 +625,30 @@ namespace MWGui //Clear the list of topics mTopicsList->clear(); - if (mPtr.getTypeName() == typeid(ESM::NPC).name()) + bool dispositionVisible = false; + if (mPtr.getClass().isNpc()) { + dispositionVisible = true; mDispositionBar->setProgressRange(100); mDispositionBar->setProgressPosition(MWBase::Environment::get().getMechanicsManager()->getDerivedDisposition(mPtr)); - mDispositionText->eraseText(0, mDispositionText->getTextLength()); - mDispositionText->addText("#B29154"+boost::lexical_cast(MWBase::Environment::get().getMechanicsManager()->getDerivedDisposition(mPtr))+std::string("/100")+"#B29154"); + mDispositionText->setCaption(boost::lexical_cast(MWBase::Environment::get().getMechanicsManager()->getDerivedDisposition(mPtr))+std::string("/100")); + } + + bool dispositionWasVisible = mDispositionBar->getVisible(); + + if (dispositionVisible && !dispositionWasVisible) + { + mDispositionBar->setVisible(true); + float offset = mDispositionBar->getHeight()+5; + mTopicsList->setCoord(mTopicsList->getCoord() + MyGUI::IntCoord(0,offset,0,-offset)); + mTopicsList->adjustSize(); + } + else if (!dispositionVisible && dispositionWasVisible) + { + mDispositionBar->setVisible(false); + float offset = mDispositionBar->getHeight()+5; + mTopicsList->setCoord(mTopicsList->getCoord() - MyGUI::IntCoord(0,offset,0,-offset)); + mTopicsList->adjustSize(); } } @@ -575,7 +656,6 @@ namespace MWGui { mLinks.push_back(new Goodbye()); mGoodbye = true; - mTopicsList->setEnabled(false); mEnabled = false; updateHistory(); } @@ -594,8 +674,7 @@ namespace MWGui + MWBase::Environment::get().getDialogueManager()->getTemporaryDispositionChange())); mDispositionBar->setProgressRange(100); mDispositionBar->setProgressPosition(disp); - mDispositionText->eraseText(0, mDispositionText->getTextLength()); - mDispositionText->addText("#B29154"+boost::lexical_cast(disp)+std::string("/100")+"#B29154"); + mDispositionText->setCaption(boost::lexical_cast(disp)+std::string("/100")); } } } diff --git a/apps/openmw/mwgui/dialogue.hpp b/apps/openmw/mwgui/dialogue.hpp index befbd6eee..23709e8fe 100644 --- a/apps/openmw/mwgui/dialogue.hpp +++ b/apps/openmw/mwgui/dialogue.hpp @@ -6,16 +6,16 @@ #include "bookpage.hpp" -#include "keywordsearch.hpp" +#include "../mwdialogue/keywordsearch.hpp" + +namespace Gui +{ + class MWList; +} namespace MWGui { class WindowManager; - - namespace Widgets - { - class MWList; - } } /* @@ -34,6 +34,7 @@ namespace MWGui PersuasionDialog(); virtual void open(); + virtual void exit(); private: MyGUI::Button* mCancelButton; @@ -75,7 +76,7 @@ namespace MWGui virtual void activated (); }; - typedef KeywordSearch KeywordSearchT; + typedef MWDialogue::KeywordSearch KeywordSearchT; struct DialogueText { @@ -103,12 +104,14 @@ namespace MWGui public: DialogueWindow(); + virtual void exit(); + // Events typedef MyGUI::delegates::CMultiDelegate0 EventHandle_Void; void notifyLinkClicked (TypesetBook::InteractiveId link); - void startDialogue(MWWorld::Ptr actor, std::string npcName); + void startDialogue(MWWorld::Ptr actor, std::string npcName, bool resetHistory); void setKeywords(std::list keyWord); void addResponse (const std::string& text, const std::string& title=""); @@ -149,6 +152,7 @@ namespace MWGui private: void updateOptions(); + void restock(); int mServices; @@ -157,7 +161,7 @@ namespace MWGui bool mGoodbye; std::vector mHistoryContents; - std::map mChoices; + std::vector > mChoices; std::vector mLinks; std::map mTopicLinks; @@ -165,9 +169,9 @@ namespace MWGui KeywordSearchT mKeywordSearch; BookPage* mHistory; - Widgets::MWList* mTopicsList; + Gui::MWList* mTopicsList; MyGUI::ScrollBar* mScrollBar; - MyGUI::ProgressPtr mDispositionBar; + MyGUI::Progress* mDispositionBar; MyGUI::EditBox* mDispositionText; PersuasionDialog mPersuasionDialog; diff --git a/apps/openmw/mwgui/enchantingdialog.cpp b/apps/openmw/mwgui/enchantingdialog.cpp index 29fe6f82d..30d67f554 100644 --- a/apps/openmw/mwgui/enchantingdialog.cpp +++ b/apps/openmw/mwgui/enchantingdialog.cpp @@ -14,6 +14,7 @@ #include "itemselection.hpp" #include "container.hpp" +#include "itemwidget.hpp" #include "sortfilteritemmodel.hpp" @@ -23,7 +24,7 @@ namespace MWGui EnchantingDialog::EnchantingDialog() : WindowBase("openmw_enchanting_dialog.layout") - , EffectEditorBase() + , EffectEditorBase(EffectEditorBase::Enchanting) , mItemSelectionDialog(NULL) { getWidget(mName, "NameEdit"); @@ -57,8 +58,46 @@ namespace MWGui void EnchantingDialog::open() { center(); - onRemoveItem(NULL); - onRemoveSoul(NULL); + } + + void EnchantingDialog::setSoulGem(const MWWorld::Ptr &gem) + { + if (gem.isEmpty()) + { + mSoulBox->setItem(MWWorld::Ptr()); + mSoulBox->clearUserStrings(); + mEnchanting.setSoulGem(MWWorld::Ptr()); + } + else + { + mSoulBox->setItem(gem); + mSoulBox->setUserString ("ToolTipType", "ItemPtr"); + mSoulBox->setUserData(gem); + mEnchanting.setSoulGem(gem); + } + } + + void EnchantingDialog::setItem(const MWWorld::Ptr &item) + { + if (item.isEmpty()) + { + mItemBox->setItem(MWWorld::Ptr()); + mItemBox->clearUserStrings(); + mEnchanting.setOldItem(MWWorld::Ptr()); + } + else + { + mName->setCaption(item.getClass().getName(item)); + mItemBox->setItem(item); + mItemBox->setUserString ("ToolTipType", "ItemPtr"); + mItemBox->setUserData(item); + mEnchanting.setOldItem(item); + } + } + + void EnchantingDialog::exit() + { + MWBase::Environment::get().getWindowManager()->removeGuiMode (GM_Enchanting); } void EnchantingDialog::updateLabels() @@ -101,9 +140,17 @@ namespace MWGui mEnchanting.setSelfEnchanting(false); mEnchanting.setEnchanter(actor); + mBuyButton->setCaptionWithReplacing("#{sBuy}"); + mPtr = actor; + setSoulGem(MWWorld::Ptr()); + setItem(MWWorld::Ptr()); + startEditing (); + mPrice->setVisible(true); + mPriceText->setVisible(true); + updateLabels(); } void EnchantingDialog::startSelfEnchanting(MWWorld::Ptr soulgem) @@ -113,20 +160,13 @@ namespace MWGui mEnchanting.setSelfEnchanting(true); mEnchanting.setEnchanter(player); + mBuyButton->setCaptionWithReplacing("#{sCreate}"); + mPtr = player; startEditing(); - mEnchanting.setSoulGem(soulgem); - - MyGUI::ImageBox* image = mSoulBox->createWidget("ImageBox", MyGUI::IntCoord(0, 0, 32, 32), MyGUI::Align::Default); - std::string path = std::string("icons\\"); - path += soulgem.getClass().getInventoryIcon(soulgem); - int pos = path.rfind("."); - path.erase(pos); - path.append(".dds"); - image->setImageTexture (path); - image->setUserString ("ToolTipType", "ItemPtr"); - image->setUserData(soulgem); - image->eventMouseButtonClick += MyGUI::newDelegate(this, &EnchantingDialog::onRemoveSoul); + + setSoulGem(soulgem); + setItem(MWWorld::Ptr()); mPrice->setVisible(false); mPriceText->setVisible(false); @@ -137,55 +177,52 @@ namespace MWGui { MWBase::Environment::get().getWindowManager()->removeGuiMode (GM_Dialogue); MWBase::Environment::get().getWindowManager()->removeGuiMode (GM_Enchanting); + resetReference(); + } + + void EnchantingDialog::resetReference() + { + ReferenceInterface::resetReference(); + setItem(MWWorld::Ptr()); + setSoulGem(MWWorld::Ptr()); + mPtr = MWWorld::Ptr(); + mEnchanting.setEnchanter(MWWorld::Ptr()); } void EnchantingDialog::onCancelButtonClicked(MyGUI::Widget* sender) { - MWBase::Environment::get().getWindowManager()->removeGuiMode (GM_Enchanting); + exit(); } void EnchantingDialog::onSelectItem(MyGUI::Widget *sender) { - delete mItemSelectionDialog; - mItemSelectionDialog = new ItemSelectionDialog("#{sEnchantItems}"); - mItemSelectionDialog->eventItemSelected += MyGUI::newDelegate(this, &EnchantingDialog::onItemSelected); - mItemSelectionDialog->eventDialogCanceled += MyGUI::newDelegate(this, &EnchantingDialog::onItemCancel); - mItemSelectionDialog->setVisible(true); - mItemSelectionDialog->openContainer(MWBase::Environment::get().getWorld()->getPlayerPtr()); - mItemSelectionDialog->setFilter(SortFilterItemModel::Filter_OnlyEnchantable); + if (mEnchanting.getOldItem().isEmpty()) + { + delete mItemSelectionDialog; + mItemSelectionDialog = new ItemSelectionDialog("#{sEnchantItems}"); + mItemSelectionDialog->eventItemSelected += MyGUI::newDelegate(this, &EnchantingDialog::onItemSelected); + mItemSelectionDialog->eventDialogCanceled += MyGUI::newDelegate(this, &EnchantingDialog::onItemCancel); + mItemSelectionDialog->setVisible(true); + mItemSelectionDialog->openContainer(MWBase::Environment::get().getWorld()->getPlayerPtr()); + mItemSelectionDialog->setFilter(SortFilterItemModel::Filter_OnlyEnchantable); + } + else + { + setItem(MWWorld::Ptr()); + updateLabels(); + } } void EnchantingDialog::onItemSelected(MWWorld::Ptr item) { mItemSelectionDialog->setVisible(false); - while (mItemBox->getChildCount ()) - MyGUI::Gui::getInstance ().destroyWidget (mItemBox->getChildAt(0)); - - MyGUI::ImageBox* image = mItemBox->createWidget("ImageBox", MyGUI::IntCoord(0, 0, 32, 32), MyGUI::Align::Default); - std::string path = std::string("icons\\"); - path += item.getClass().getInventoryIcon(item); - int pos = path.rfind("."); - path.erase(pos); - path.append(".dds"); - image->setImageTexture (path); - image->setUserString ("ToolTipType", "ItemPtr"); - image->setUserData(item); - image->eventMouseButtonClick += MyGUI::newDelegate(this, &EnchantingDialog::onRemoveItem); - - mEnchanting.setOldItem(item); + setItem(item); + MWBase::Environment::get().getSoundManager()->playSound(item.getClass().getDownSoundId(item), 1, 1); mEnchanting.nextCastStyle(); updateLabels(); } - void EnchantingDialog::onRemoveItem(MyGUI::Widget *sender) - { - while (mItemBox->getChildCount ()) - MyGUI::Gui::getInstance ().destroyWidget (mItemBox->getChildAt(0)); - mEnchanting.setOldItem(MWWorld::Ptr()); - updateLabels(); - } - void EnchantingDialog::onItemCancel() { mItemSelectionDialog->setVisible(false); @@ -194,35 +231,16 @@ namespace MWGui void EnchantingDialog::onSoulSelected(MWWorld::Ptr item) { mItemSelectionDialog->setVisible(false); - mEnchanting.setSoulGem(item); + mEnchanting.setSoulGem(item); if(mEnchanting.getGemCharge()==0) { MWBase::Environment::get().getWindowManager()->messageBox ("#{sNotifyMessage32}"); return; } - while (mSoulBox->getChildCount ()) - MyGUI::Gui::getInstance ().destroyWidget (mSoulBox->getChildAt(0)); - - MyGUI::ImageBox* image = mSoulBox->createWidget("ImageBox", MyGUI::IntCoord(0, 0, 32, 32), MyGUI::Align::Default); - std::string path = std::string("icons\\"); - path += item.getClass().getInventoryIcon(item); - int pos = path.rfind("."); - path.erase(pos); - path.append(".dds"); - image->setImageTexture (path); - image->setUserString ("ToolTipType", "ItemPtr"); - image->setUserData(item); - image->eventMouseButtonClick += MyGUI::newDelegate(this, &EnchantingDialog::onRemoveSoul); - updateLabels(); - } - - void EnchantingDialog::onRemoveSoul(MyGUI::Widget *sender) - { - while (mSoulBox->getChildCount ()) - MyGUI::Gui::getInstance ().destroyWidget (mSoulBox->getChildAt(0)); - mEnchanting.setSoulGem(MWWorld::Ptr()); + setSoulGem(item); + MWBase::Environment::get().getSoundManager()->playSound(item.getClass().getDownSoundId(item), 1, 1); updateLabels(); } @@ -233,15 +251,23 @@ namespace MWGui void EnchantingDialog::onSelectSoul(MyGUI::Widget *sender) { - delete mItemSelectionDialog; - mItemSelectionDialog = new ItemSelectionDialog("#{sSoulGemsWithSouls}"); - mItemSelectionDialog->eventItemSelected += MyGUI::newDelegate(this, &EnchantingDialog::onSoulSelected); - mItemSelectionDialog->eventDialogCanceled += MyGUI::newDelegate(this, &EnchantingDialog::onSoulCancel); - mItemSelectionDialog->setVisible(true); - mItemSelectionDialog->openContainer(MWBase::Environment::get().getWorld()->getPlayerPtr()); - mItemSelectionDialog->setFilter(SortFilterItemModel::Filter_OnlyChargedSoulstones); - - //MWBase::Environment::get().getWindowManager()->messageBox("#{sInventorySelectNoSoul}"); + if (mEnchanting.getGem().isEmpty()) + { + delete mItemSelectionDialog; + mItemSelectionDialog = new ItemSelectionDialog("#{sSoulGemsWithSouls}"); + mItemSelectionDialog->eventItemSelected += MyGUI::newDelegate(this, &EnchantingDialog::onSoulSelected); + mItemSelectionDialog->eventDialogCanceled += MyGUI::newDelegate(this, &EnchantingDialog::onSoulCancel); + mItemSelectionDialog->setVisible(true); + mItemSelectionDialog->openContainer(MWBase::Environment::get().getWorld()->getPlayerPtr()); + mItemSelectionDialog->setFilter(SortFilterItemModel::Filter_OnlyChargedSoulstones); + + //MWBase::Environment::get().getWindowManager()->messageBox("#{sInventorySelectNoSoul}"); + } + else + { + setSoulGem(MWWorld::Ptr()); + updateLabels(); + } } void EnchantingDialog::notifyEffectsChanged () @@ -294,7 +320,7 @@ namespace MWGui MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); int playerGold = player.getClass().getContainerStore(player).count(MWWorld::ContainerStore::sGoldId); - if (mEnchanting.getEnchantPrice() > playerGold) + if (mPtr != player && mEnchanting.getEnchantPrice() > playerGold) { MWBase::Environment::get().getWindowManager()->messageBox ("#{sNotifyMessage18}"); return; @@ -316,7 +342,7 @@ namespace MWGui MWBase::Environment::get().getMechanicsManager()->reportCrime(player, mPtr, MWBase::MechanicsManager::OT_Theft, item.getClass().getValue(item)); MWBase::Environment::get().getWindowManager()->removeGuiMode (GM_Enchanting); - MWBase::Environment::get().getWindowManager()->removeGuiMode (GM_Dialogue); + MWBase::Environment::get().getDialogueManager()->goodbyeSelected(); return; } } diff --git a/apps/openmw/mwgui/enchantingdialog.hpp b/apps/openmw/mwgui/enchantingdialog.hpp index 8bad60c8e..5b67d199b 100644 --- a/apps/openmw/mwgui/enchantingdialog.hpp +++ b/apps/openmw/mwgui/enchantingdialog.hpp @@ -11,6 +11,7 @@ namespace MWGui { class ItemSelectionDialog; + class ItemWidget; class EnchantingDialog : public WindowBase, public ReferenceInterface, public EffectEditorBase { @@ -19,9 +20,17 @@ namespace MWGui virtual ~EnchantingDialog(); virtual void open(); + + virtual void exit(); + + void setSoulGem (const MWWorld::Ptr& gem); + void setItem (const MWWorld::Ptr& item); + void startEnchanting(MWWorld::Ptr actor); void startSelfEnchanting(MWWorld::Ptr soulgem); + virtual void resetReference(); + protected: virtual void onReferenceUnavailable(); virtual void notifyEffectsChanged (); @@ -29,8 +38,6 @@ namespace MWGui void onCancelButtonClicked(MyGUI::Widget* sender); void onSelectItem (MyGUI::Widget* sender); void onSelectSoul (MyGUI::Widget* sender); - void onRemoveItem (MyGUI::Widget* sender); - void onRemoveSoul (MyGUI::Widget* sender); void onItemSelected(MWWorld::Ptr item); void onItemCancel(); @@ -43,8 +50,8 @@ namespace MWGui ItemSelectionDialog* mItemSelectionDialog; MyGUI::Button* mCancelButton; - MyGUI::ImageBox* mItemBox; - MyGUI::ImageBox* mSoulBox; + ItemWidget* mItemBox; + ItemWidget* mSoulBox; MyGUI::Button* mTypeButton; MyGUI::Button* mBuyButton; diff --git a/apps/openmw/mwgui/fontloader.hpp b/apps/openmw/mwgui/fontloader.hpp deleted file mode 100644 index 7954b0875..000000000 --- a/apps/openmw/mwgui/fontloader.hpp +++ /dev/null @@ -1,25 +0,0 @@ -#ifndef MWGUI_FONTLOADER_H -#define MWGUI_FONTLOADER_H - -#include - -namespace MWGui -{ - - - /// @brief loads Morrowind's .fnt/.tex fonts for use with MyGUI and Ogre - class FontLoader - { - public: - FontLoader (ToUTF8::FromType encoding); - void loadAllFonts (); - - private: - ToUTF8::FromType mEncoding; - - void loadFont (const std::string& fileName); - }; - -} - -#endif diff --git a/apps/openmw/mwgui/formatting.cpp b/apps/openmw/mwgui/formatting.cpp index 4d3d04ced..c55650c45 100644 --- a/apps/openmw/mwgui/formatting.cpp +++ b/apps/openmw/mwgui/formatting.cpp @@ -1,419 +1,459 @@ #include "formatting.hpp" #include +#include +#include #include "../mwscript/interpretercontext.hpp" #include #include -#include #include -#include -#include -#include +#include #include -namespace +#include + +namespace MWGui { - int convertFromHex(std::string hex) + namespace Formatting { - int value = 0; + /* BookTextParser */ + BookTextParser::BookTextParser(const std::string & text) + : mIndex(0), mText(text), mIgnoreNewlineTags(true), mIgnoreLineEndings(true), mClosingTag(false) + { + MWScript::InterpreterContext interpreterContext(NULL, MWWorld::Ptr()); // empty arguments, because there is no locals or actor + mText = Interpreter::fixDefinesBook(mText, interpreterContext); - int a = 0; - int b = hex.length() - 1; - for (; b >= 0; a++, b--) + boost::algorithm::replace_all(mText, "\r", ""); + + registerTag("br", Event_BrTag); + registerTag("p", Event_PTag); + registerTag("img", Event_ImgTag); + registerTag("div", Event_DivTag); + registerTag("font", Event_FontTag); + } + + void BookTextParser::registerTag(const std::string & tag, BookTextParser::Events type) { - if (hex[b] >= '0' && hex[b] <= '9') - { - value += (hex[b] - '0') * (1 << (a * 4)); - } - else + mTagTypes[tag] = type; + } + + std::string BookTextParser::getReadyText() const + { + return mReadyText; + } + + BookTextParser::Events BookTextParser::next() + { + while (mIndex < mText.size()) { - switch (hex[b]) + char ch = mText[mIndex]; + if (ch == '<') { - case 'A': - case 'a': - value += 10 * (1 << (a * 4)); - break; + const size_t tagStart = mIndex + 1; + const size_t tagEnd = mText.find('>', tagStart); + if (tagEnd == std::string::npos) + throw std::runtime_error("BookTextParser Error: Tag is not terminated"); + parseTag(mText.substr(tagStart, tagEnd - tagStart)); + mIndex = tagEnd; - case 'B': - case 'b': - value += 11 * (1 << (a * 4)); - break; + if (mTagTypes.find(mTag) != mTagTypes.end()) + { + Events type = mTagTypes.at(mTag); + + if (type == Event_BrTag || type == Event_PTag) + { + if (!mIgnoreNewlineTags) + { + if (type == Event_BrTag) + mBuffer.push_back('\n'); + else + { + mBuffer.append("\n\n"); + } + } + mIgnoreLineEndings = true; + } + else + flushBuffer(); - case 'C': - case 'c': - value += 12 * (1 << (a * 4)); - break; + if (type == Event_ImgTag) + { + mIgnoreNewlineTags = false; + } - case 'D': - case 'd': - value += 13 * (1 << (a * 4)); - break; + ++mIndex; + return type; + } + } + else + { + if (!mIgnoreLineEndings || ch != '\n') + { + mBuffer.push_back(ch); + mIgnoreLineEndings = false; + mIgnoreNewlineTags = false; + } + } - case 'E': - case 'e': - value += 14 * (1 << (a * 4)); - break; + ++mIndex; + } - case 'F': - case 'f': - value += 15 * (1 << (a * 4)); - break; + flushBuffer(); + return Event_EOF; + } - default: - throw std::runtime_error("invalid character in hex number"); - break; - } - } + void BookTextParser::flushBuffer() + { + mReadyText = mBuffer; + mBuffer.clear(); } - return value; - } + const BookTextParser::Attributes & BookTextParser::getAttributes() const + { + return mAttributes; + } - Ogre::UTFString::unicode_char unicodeCharFromChar(char ch) - { - std::string s; - s += ch; - Ogre::UTFString string(s); - return string.getChar(0); - } - - bool is_not_empty(const std::string& s) { - std::string temp = s; - boost::algorithm::trim(temp); - return !temp.empty(); - } -} + bool BookTextParser::isClosingTag() const + { + return mClosingTag; + } -namespace MWGui -{ + void BookTextParser::parseTag(std::string tag) + { + size_t tagNameEndPos = tag.find(' '); + mAttributes.clear(); + mTag = tag.substr(0, tagNameEndPos); + Misc::StringUtils::toLower(mTag); + if (mTag.empty()) + return; + + mClosingTag = (mTag[0] == '/'); + if (mClosingTag) + { + mTag.erase(mTag.begin()); + return; + } - std::vector BookTextParser::split(std::string utf8Text, const int width, const int height) - { - using Ogre::UTFString; - std::vector result; + if (tagNameEndPos == std::string::npos) + return; + tag.erase(0, tagNameEndPos+1); - MWScript::InterpreterContext interpreterContext(NULL, MWWorld::Ptr()); // empty arguments, because there is no locals or actor - utf8Text = Interpreter::fixDefinesBook(utf8Text, interpreterContext); + while (!tag.empty()) + { + size_t sepPos = tag.find('='); + if (sepPos == std::string::npos) + return; + + std::string key = tag.substr(0, sepPos); + Misc::StringUtils::toLower(key); + tag.erase(0, sepPos+1); + + std::string value; - boost::algorithm::replace_all(utf8Text, "\n", ""); - boost::algorithm::replace_all(utf8Text, "\r", ""); - boost::algorithm::replace_all(utf8Text, "
", "\n"); - boost::algorithm::replace_all(utf8Text, "

", "\n\n"); + if (tag.empty()) + return; - UTFString text(utf8Text); - const int spacing = 48; + if (tag[0] == '"') + { + size_t quoteEndPos = tag.find('"', 1); + if (quoteEndPos == std::string::npos) + throw std::runtime_error("BookTextParser Error: Missing end quote in tag"); + value = tag.substr(1, quoteEndPos-1); + tag.erase(0, quoteEndPos+2); + } + else + { + size_t valEndPos = tag.find(' '); + if (valEndPos == std::string::npos) + { + value = tag; + tag.erase(); + } + else + { + value = tag.substr(0, valEndPos); + tag.erase(0, valEndPos+1); + } + } - const UTFString::unicode_char LEFT_ANGLE = unicodeCharFromChar('<'); - const UTFString::unicode_char NEWLINE = unicodeCharFromChar('\n'); - const UTFString::unicode_char SPACE = unicodeCharFromChar(' '); + mAttributes[key] = value; + } + } - while (!text.empty()) + /* BookFormatter */ + Paginator::Pages BookFormatter::markupToWidget(MyGUI::Widget * parent, const std::string & markup, const int pageWidth, const int pageHeight) { - // read in characters until we have exceeded the size, or run out of text - int currentWidth = 0; - int currentHeight = 0; + Paginator pag(pageWidth, pageHeight); - size_t currentWordStart = 0; - size_t index = 0; - + while (parent->getChildCount()) { - std::string texToTrim = text.asUTF8(); - boost::algorithm::trim( texToTrim ); - text = UTFString(texToTrim); + MyGUI::Gui::getInstance().destroyWidget(parent->getChildAt(0)); } - - - while (currentHeight <= height - spacing && index < text.size()) + + MyGUI::Widget * paper = parent->createWidget("Widget", MyGUI::IntCoord(0, 0, pag.getPageWidth(), pag.getPageHeight()), MyGUI::Align::Left | MyGUI::Align::Top); + paper->setNeedMouseFocus(false); + + BookTextParser parser(markup); + + bool brBeforeLastTag = false; + bool isPrevImg = false; + for (;;) { - const UTFString::unicode_char ch = text.getChar(index); - if (ch == LEFT_ANGLE) - { - const size_t tagStart = index + 1; - const size_t tagEnd = text.find('>', tagStart); - if (tagEnd == UTFString::npos) - throw std::runtime_error("BookTextParser Error: Tag is not terminated"); - const std::string tag = text.substr(tagStart, tagEnd - tagStart).asUTF8(); + BookTextParser::Events event = parser.next(); + if (event == BookTextParser::Event_BrTag || event == BookTextParser::Event_PTag) + continue; - if (boost::algorithm::starts_with(tag, "IMG")) - { - const int h = mHeight; - parseImage(tag, false); - currentHeight += (mHeight - h); - currentWidth = 0; - } - else if (boost::algorithm::starts_with(tag, "FONT")) + std::string plainText = parser.getReadyText(); + if (plainText.empty()) + brBeforeLastTag = false; + else + { + // Each block of text (between two tags / boundary and tag) will be displayed in a separate editbox widget, + // which means an additional linebreak will be created between them. + // ^ This is not what vanilla MW assumes, so we must deal with line breaks around tags appropriately. + bool brAtStart = (plainText[0] == '\n'); + bool brAtEnd = (plainText[plainText.size()-1] == '\n'); + + if (brAtStart && !brBeforeLastTag && !isPrevImg) + plainText.erase(plainText.begin()); + + if (plainText.size() && brAtEnd) + plainText.erase(plainText.end()-1); + +#if (MYGUI_VERSION < MYGUI_DEFINE_VERSION(3, 2, 2)) + // splitting won't be fully functional until 3.2.2 (see TextElement::pageSplit()) + // hack: prevent newlines at the end of the book possibly creating unnecessary pages + if (event == BookTextParser::Event_EOF) { - parseFont(tag); - if (currentWidth != 0) { - currentHeight += currentFontHeight(); - currentWidth = 0; - } - currentWidth = 0; + while (plainText.size() && plainText[plainText.size()-1] == '\n') + plainText.erase(plainText.end()-1); } - else if (boost::algorithm::starts_with(tag, "DIV")) +#endif + + if (!plainText.empty() || brBeforeLastTag || isPrevImg) { - parseDiv(tag); - if (currentWidth != 0) { - currentHeight += currentFontHeight(); - currentWidth = 0; - } + TextElement elem(paper, pag, mBlockStyle, + mTextStyle, plainText); + elem.paginate(); } - index = tagEnd; - } - else if (ch == NEWLINE) - { - currentHeight += currentFontHeight(); - currentWidth = 0; - currentWordStart = index; - } - else if (ch == SPACE) - { - currentWidth += 3; // keep this in sync with the font's SpaceWidth property - currentWordStart = index; - } - else - { - currentWidth += widthForCharGlyph(ch); - } - if (currentWidth > width) - { - currentHeight += currentFontHeight(); - currentWidth = 0; - // add size of the current word - UTFString word = text.substr(currentWordStart, index - currentWordStart); - for (UTFString::const_iterator it = word.begin(), end = word.end(); it != end; ++it) - currentWidth += widthForCharGlyph(it.getCharacter()); + brBeforeLastTag = brAtEnd; } - index += UTFString::_utf16_char_length(ch); - } - const size_t pageEnd = (currentHeight > height - spacing && currentWordStart != 0) - ? currentWordStart : index; - result.push_back(text.substr(0, pageEnd).asUTF8()); - text.erase(0, pageEnd); - } - - std::vector nonEmptyPages; - boost::copy(result | boost::adaptors::filtered(is_not_empty), std::back_inserter(nonEmptyPages)); - return nonEmptyPages; - } + if (event == BookTextParser::Event_EOF) + break; - float BookTextParser::widthForCharGlyph(unsigned unicodeChar) const - { - std::string fontName(mTextStyle.mFont == "Default" ? MyGUI::FontManager::getInstance().getDefaultFont() : mTextStyle.mFont); - return MyGUI::FontManager::getInstance().getByName(fontName) - ->getGlyphInfo(unicodeChar)->width; - } + isPrevImg = (event == BookTextParser::Event_ImgTag); - float BookTextParser::currentFontHeight() const - { - std::string fontName(mTextStyle.mFont == "Default" ? MyGUI::FontManager::getInstance().getDefaultFont() : mTextStyle.mFont); - return MyGUI::FontManager::getInstance().getByName(fontName)->getDefaultHeight(); - } + switch (event) + { + case BookTextParser::Event_ImgTag: + { + const BookTextParser::Attributes & attr = parser.getAttributes(); - MyGUI::IntSize BookTextParser::parsePage(std::string text, MyGUI::Widget* parent, const int width) - { - MWScript::InterpreterContext interpreterContext(NULL, MWWorld::Ptr()); // empty arguments, because there is no locals or actor - text = Interpreter::fixDefinesBook(text, interpreterContext); + if (attr.find("src") == attr.end() || attr.find("width") == attr.end() || attr.find("height") == attr.end()) + continue; - mParent = parent; - mWidth = width; - mHeight = 0; + std::string src = attr.at("src"); + int width = boost::lexical_cast(attr.at("width")); + int height = boost::lexical_cast(attr.at("height")); - assert(mParent); - while (mParent->getChildCount()) - { - MyGUI::Gui::getInstance().destroyWidget(mParent->getChildAt(0)); - } + ImageElement elem(paper, pag, mBlockStyle, + src, width, height); + elem.paginate(); + break; + } + case BookTextParser::Event_FontTag: + if (parser.isClosingTag()) + resetFontProperties(); + else + handleFont(parser.getAttributes()); + break; + case BookTextParser::Event_DivTag: + handleDiv(parser.getAttributes()); + break; + default: + break; + } + } - // remove trailing " - if (text[text.size()-1] == '\"') - text.erase(text.size()-1); + // insert last page + if (pag.getStartTop() != pag.getCurrentTop()) + pag << Paginator::Page(pag.getStartTop(), pag.getStartTop() + pag.getPageHeight()); - parseSubText(text); - return MyGUI::IntSize(mWidth, mHeight); - } - - MyGUI::IntSize BookTextParser::parseScroll(std::string text, MyGUI::Widget* parent, const int width) - { - MWScript::InterpreterContext interpreterContext(NULL, MWWorld::Ptr()); // empty arguments, because there is no locals or actor - text = Interpreter::fixDefinesBook(text, interpreterContext); + paper->setSize(paper->getWidth(), pag.getCurrentTop()); - mParent = parent; - mWidth = width; - mHeight = 0; + return pag.getPages(); + } - assert(mParent); - while (mParent->getChildCount()) + Paginator::Pages BookFormatter::markupToWidget(MyGUI::Widget * parent, const std::string & markup) { - MyGUI::Gui::getInstance().destroyWidget(mParent->getChildAt(0)); + return markupToWidget(parent, markup, parent->getWidth(), parent->getHeight()); } - boost::algorithm::replace_all(text, "
", "\n"); - boost::algorithm::replace_all(text, "

", "\n\n"); - boost::algorithm::trim_left(text); - - // remove trailing " - if (text[text.size()-1] == '\"') - text.erase(text.size()-1); - - parseSubText(text); - return MyGUI::IntSize(mWidth, mHeight); - } - - - void BookTextParser::parseImage(std::string tag, bool createWidget) - { - int src_start = tag.find("SRC=")+5; - std::string image = tag.substr(src_start, tag.find('"', src_start)-src_start); - - // fix texture extension to .dds - if (image.size() > 4) + void BookFormatter::resetFontProperties() { - image[image.size()-3] = 'd'; - image[image.size()-2] = 'd'; - image[image.size()-1] = 's'; + mTextStyle = TextStyle(); } - int width_start = tag.find("WIDTH=")+7; - int width = boost::lexical_cast(tag.substr(width_start, tag.find('"', width_start)-width_start)); + void BookFormatter::handleDiv(const BookTextParser::Attributes & attr) + { + if (attr.find("align") == attr.end()) + return; - int height_start = tag.find("HEIGHT=")+8; - int height = boost::lexical_cast(tag.substr(height_start, tag.find('"', height_start)-height_start)); + std::string align = attr.at("align"); - if (createWidget) - { - MyGUI::ImageBox* box = mParent->createWidget ("ImageBox", - MyGUI::IntCoord(0, mHeight, width, height), MyGUI::Align::Left | MyGUI::Align::Top, - mParent->getName() + boost::lexical_cast(mParent->getChildCount())); + if (Misc::StringUtils::ciEqual(align, "center")) + mBlockStyle.mAlign = MyGUI::Align::HCenter; + else if (Misc::StringUtils::ciEqual(align, "left")) + mBlockStyle.mAlign = MyGUI::Align::Left; + else if (Misc::StringUtils::ciEqual(align, "right")) + mBlockStyle.mAlign = MyGUI::Align::Right; + } - // Apparently a bug with some morrowind versions, they reference the image without the size suffix. - // So if the image isn't found, try appending the size. - if (!Ogre::ResourceGroupManager::getSingleton().resourceExistsInAnyGroup("bookart\\"+image)) + void BookFormatter::handleFont(const BookTextParser::Attributes & attr) + { + if (attr.find("color") != attr.end()) { - std::stringstream str; - str << image.substr(0, image.rfind(".")) << "_" << width << "_" << height << image.substr(image.rfind(".")); - image = str.str(); + int color; + std::stringstream ss; + ss << attr.at("color"); + ss >> std::hex >> color; + + mTextStyle.mColour = MyGUI::Colour( + (color>>16 & 0xFF) / 255.f, + (color>>8 & 0xFF) / 255.f, + (color & 0xFF) / 255.f); } + if (attr.find("face") != attr.end()) + { + std::string face = attr.at("face"); - box->setImageTexture("bookart\\" + image); - box->setProperty("NeedMouse", "false"); + if (face != "Magic Cards") + mTextStyle.mFont = face; + } + if (attr.find("size") != attr.end()) + { + /// \todo + } } - mWidth = std::max(mWidth, width); - mHeight += height; - } - - void BookTextParser::parseDiv(std::string tag) - { - if (tag.find("ALIGN=") == std::string::npos) - return; - - int align_start = tag.find("ALIGN=")+7; - std::string align = tag.substr(align_start, tag.find('"', align_start)-align_start); - if (align == "CENTER") - mTextStyle.mTextAlign = MyGUI::Align::HCenter; - else if (align == "LEFT") - mTextStyle.mTextAlign = MyGUI::Align::Left; - } + /* GraphicElement */ + GraphicElement::GraphicElement(MyGUI::Widget * parent, Paginator & pag, const BlockStyle & blockStyle) + : mParent(parent), mPaginator(pag), mBlockStyle(blockStyle) + { + } - void BookTextParser::parseFont(std::string tag) - { - if (tag.find("COLOR=") != std::string::npos) + void GraphicElement::paginate() { - int color_start = tag.find("COLOR=")+7; - std::string color = tag.substr(color_start, tag.find('"', color_start)-color_start); + int newTop = mPaginator.getCurrentTop() + getHeight(); + while (newTop-mPaginator.getStartTop() > mPaginator.getPageHeight()) + { + int newStartTop = pageSplit(); + mPaginator << Paginator::Page(mPaginator.getStartTop(), newStartTop); + mPaginator.setStartTop(newStartTop); + } - mTextStyle.mColour = MyGUI::Colour( - convertFromHex(color.substr(0, 2))/255.0, - convertFromHex(color.substr(2, 2))/255.0, - convertFromHex(color.substr(4, 2))/255.0); + mPaginator.setCurrentTop(newTop); } - if (tag.find("FACE=") != std::string::npos) + + int GraphicElement::pageSplit() { - int face_start = tag.find("FACE=")+6; - std::string face = tag.substr(face_start, tag.find('"', face_start)-face_start); + return mPaginator.getStartTop() + mPaginator.getPageHeight(); + } - if (face != "Magic Cards") - mTextStyle.mFont = face; + /* TextElement */ + TextElement::TextElement(MyGUI::Widget * parent, Paginator & pag, const BlockStyle & blockStyle, + const TextStyle & textStyle, const std::string & text) + : GraphicElement(parent, pag, blockStyle), + mTextStyle(textStyle) + { + MyGUI::EditBox* box = parent->createWidget("NormalText", + MyGUI::IntCoord(0, pag.getCurrentTop(), pag.getPageWidth(), 0), MyGUI::Align::Left | MyGUI::Align::Top, + parent->getName() + boost::lexical_cast(parent->getChildCount())); + box->setProperty("Static", "true"); + box->setProperty("MultiLine", "true"); + box->setProperty("WordWrap", "true"); + box->setProperty("NeedMouse", "false"); + box->setMaxTextLength(text.size()); + box->setTextAlign(mBlockStyle.mAlign); + box->setTextColour(mTextStyle.mColour); + box->setFontName(mTextStyle.mFont); + box->setCaption(MyGUI::TextIterator::toTagsString(text)); + box->setSize(box->getSize().width, box->getTextSize().height); + mEditBox = box; } - if (tag.find("SIZE=") != std::string::npos) + + int TextElement::currentFontHeight() const { - /// \todo + std::string fontName(mTextStyle.mFont == "Default" ? MyGUI::FontManager::getInstance().getDefaultFont() : mTextStyle.mFont); + return MyGUI::FontManager::getInstance().getByName(fontName)->getDefaultHeight(); } - } - void BookTextParser::parseSubText(std::string text) - { - if (text[0] == '<') + int TextElement::getHeight() { - const size_t tagStart = 1; - const size_t tagEnd = text.find('>', tagStart); - if (tagEnd == std::string::npos) - throw std::runtime_error("BookTextParser Error: Tag is not terminated"); - const std::string tag = text.substr(tagStart, tagEnd - tagStart); - - if (boost::algorithm::starts_with(tag, "IMG")) - parseImage(tag); - if (boost::algorithm::starts_with(tag, "FONT")) - parseFont(tag); - if (boost::algorithm::starts_with(tag, "DIV")) - parseDiv(tag); - - text.erase(0, tagEnd + 1); + return mEditBox->getTextSize().height; } - size_t tagStart = std::string::npos; - std::string realText; // real text, without tags - for (size_t i = 0; i= MYGUI_DEFINE_VERSION(3, 2, 2)) + const MyGUI::VectorLineInfo & lines = mEditBox->getSubWidgetText()->castType()->getLineInfo(); + for (unsigned int i = lastLine; i < lines.size(); ++i) { - if ((i + 1 < text.size()) && text[i+1] == '/') // ignore closing tags - { - while (c != '>') - { - if (i >= text.size()) - throw std::runtime_error("BookTextParser Error: Tag is not terminated"); - ++i; - c = text[i]; - } - continue; - } + if (lines[i].width == 0) + ret += lineHeight; else - { - tagStart = i; break; - } } - else - realText += c; +#endif + return ret; } - MyGUI::EditBox* box = mParent->createWidget("NormalText", - MyGUI::IntCoord(0, mHeight, mWidth, 24), MyGUI::Align::Left | MyGUI::Align::Top, - mParent->getName() + boost::lexical_cast(mParent->getChildCount())); - box->setProperty("Static", "true"); - box->setProperty("MultiLine", "true"); - box->setProperty("WordWrap", "true"); - box->setProperty("NeedMouse", "false"); - box->setMaxTextLength(realText.size()); - box->setTextAlign(mTextStyle.mTextAlign); - box->setTextColour(mTextStyle.mColour); - box->setFontName(mTextStyle.mFont); - box->setCaption(realText); - box->setSize(box->getSize().width, box->getTextSize().height); - mHeight += box->getTextSize().height; - - if (tagStart != std::string::npos) + /* ImageElement */ + ImageElement::ImageElement(MyGUI::Widget * parent, Paginator & pag, const BlockStyle & blockStyle, + const std::string & src, int width, int height) + : GraphicElement(parent, pag, blockStyle), + mImageHeight(height) { - parseSubText(text.substr(tagStart, text.size())); + int left = 0; + if (mBlockStyle.mAlign.isHCenter()) + left += (pag.getPageWidth() - width) / 2; + else if (mBlockStyle.mAlign.isLeft()) + left = 0; + else if (mBlockStyle.mAlign.isRight()) + left += pag.getPageWidth() - width; + + mImageBox = parent->createWidget ("ImageBox", + MyGUI::IntCoord(left, pag.getCurrentTop(), width, mImageHeight), MyGUI::Align::Left | MyGUI::Align::Top, + parent->getName() + boost::lexical_cast(parent->getChildCount())); + + std::string image = Misc::ResourceHelpers::correctBookartPath(src, width, mImageHeight); + mImageBox->setImageTexture(image); + mImageBox->setProperty("NeedMouse", "false"); + } + + int ImageElement::getHeight() + { + return mImageHeight; } - } + int ImageElement::pageSplit() + { + // if the image is larger than the page, fall back to the default pageSplit implementation + if (mImageHeight > mPaginator.getPageHeight()) + return GraphicElement::pageSplit(); + return mPaginator.getCurrentTop(); + } + } } diff --git a/apps/openmw/mwgui/formatting.hpp b/apps/openmw/mwgui/formatting.hpp index a32d98fe5..5b7925057 100644 --- a/apps/openmw/mwgui/formatting.hpp +++ b/apps/openmw/mwgui/formatting.hpp @@ -2,66 +2,168 @@ #define MWGUI_FORMATTING_H #include +#include namespace MWGui { - struct TextStyle + namespace Formatting { - TextStyle() : - mColour(0,0,0) - , mFont("Default") - , mTextSize(16) - , mTextAlign(MyGUI::Align::Left | MyGUI::Align::Top) + struct TextStyle { - } + TextStyle() : + mColour(0,0,0) + , mFont("Default") + , mTextSize(16) + { + } - MyGUI::Colour mColour; - std::string mFont; - int mTextSize; - MyGUI::Align mTextAlign; - }; + MyGUI::Colour mColour; + std::string mFont; + int mTextSize; + }; - /// \brief utilities for parsing book/scroll text as mygui widgets - class BookTextParser - { - public: - /** - * Parse markup as MyGUI widgets - * @param markup to parse - * @param parent for the created widgets - * @param maximum width - * @return size of the created widgets - */ - MyGUI::IntSize parsePage(std::string text, MyGUI::Widget* parent, const int width); - - /** - * Parse markup as MyGUI widgets - * @param markup to parse - * @param parent for the created widgets - * @param maximum width - * @return size of the created widgets - */ - MyGUI::IntSize parseScroll(std::string text, MyGUI::Widget* parent, const int width); - - /** - * Split the specified text into pieces that fit in the area specified by width and height parameters - */ - std::vector split(std::string text, const int width, const int height); - - protected: - float widthForCharGlyph(unsigned unicodeChar) const; - float currentFontHeight() const; - void parseSubText(std::string text); - - void parseImage(std::string tag, bool createWidget=true); - void parseDiv(std::string tag); - void parseFont(std::string tag); - private: - MyGUI::Widget* mParent; - int mWidth; // maximum width - int mHeight; // current height - TextStyle mTextStyle; - }; + struct BlockStyle + { + BlockStyle() : + mAlign(MyGUI::Align::Left | MyGUI::Align::Top) + { + } + + MyGUI::Align mAlign; + }; + + class BookTextParser + { + public: + typedef std::map Attributes; + enum Events + { + Event_None = -2, + Event_EOF = -1, + Event_BrTag, + Event_PTag, + Event_ImgTag, + Event_DivTag, + Event_FontTag + }; + + BookTextParser(const std::string & text); + + Events next(); + + const Attributes & getAttributes() const; + std::string getReadyText() const; + bool isClosingTag() const; + + private: + void registerTag(const std::string & tag, Events type); + void flushBuffer(); + void parseTag(std::string tag); + + size_t mIndex; + std::string mText; + std::string mReadyText; + + bool mIgnoreNewlineTags; + bool mIgnoreLineEndings; + Attributes mAttributes; + std::string mTag; + bool mClosingTag; + std::map mTagTypes; + std::string mBuffer; + }; + + class Paginator + { + public: + typedef std::pair Page; + typedef std::vector Pages; + + Paginator(int pageWidth, int pageHeight) + : mStartTop(0), mCurrentTop(0), + mPageWidth(pageWidth), mPageHeight(pageHeight) + { + } + + int getStartTop() const { return mStartTop; } + int getCurrentTop() const { return mCurrentTop; } + int getPageWidth() const { return mPageWidth; } + int getPageHeight() const { return mPageHeight; } + Pages getPages() const { return mPages; } + + void setStartTop(int top) { mStartTop = top; } + void setCurrentTop(int top) { mCurrentTop = top; } + + Paginator & operator<<(const Page & page) + { + mPages.push_back(page); + return *this; + } + + private: + int mStartTop, mCurrentTop; + int mPageWidth, mPageHeight; + Pages mPages; + }; + + /// \brief utilities for parsing book/scroll text as mygui widgets + class BookFormatter + { + public: + Paginator::Pages markupToWidget(MyGUI::Widget * parent, const std::string & markup, const int pageWidth, const int pageHeight); + Paginator::Pages markupToWidget(MyGUI::Widget * parent, const std::string & markup); + + private: + void resetFontProperties(); + + void handleDiv(const BookTextParser::Attributes & attr); + void handleFont(const BookTextParser::Attributes & attr); + + TextStyle mTextStyle; + BlockStyle mBlockStyle; + }; + + class GraphicElement + { + public: + GraphicElement(MyGUI::Widget * parent, Paginator & pag, const BlockStyle & blockStyle); + virtual int getHeight() = 0; + virtual void paginate(); + virtual int pageSplit(); + + protected: + virtual ~GraphicElement() {} + MyGUI::Widget * mParent; + Paginator & mPaginator; + BlockStyle mBlockStyle; + }; + + class TextElement : public GraphicElement + { + public: + TextElement(MyGUI::Widget * parent, Paginator & pag, const BlockStyle & blockStyle, + const TextStyle & textStyle, const std::string & text); + virtual int getHeight(); + virtual int pageSplit(); + private: + int currentFontHeight() const; + TextStyle mTextStyle; + MyGUI::EditBox * mEditBox; + }; + + class ImageElement : public GraphicElement + { + public: + ImageElement(MyGUI::Widget * parent, Paginator & pag, const BlockStyle & blockStyle, + const std::string & src, int width, int height); + virtual int getHeight(); + virtual int pageSplit(); + + private: + int mImageHeight; + MyGUI::ImageBox * mImageBox; + }; + } } #endif diff --git a/apps/openmw/mwgui/hud.cpp b/apps/openmw/mwgui/hud.cpp index ede5750a5..2593e6e77 100644 --- a/apps/openmw/mwgui/hud.cpp +++ b/apps/openmw/mwgui/hud.cpp @@ -2,6 +2,8 @@ #include +#include + #include "../mwbase/environment.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwbase/windowmanager.hpp" @@ -18,6 +20,7 @@ #include "container.hpp" #include "itemmodel.hpp" +#include "itemwidget.hpp" namespace MWGui { @@ -58,8 +61,9 @@ namespace MWGui }; - HUD::HUD(int width, int height, int fpsLevel, DragAndDrop* dragAndDrop) + HUD::HUD(CustomMarkerCollection &customMarkers, int fpsLevel, DragAndDrop* dragAndDrop) : Layout("openmw_hud.layout") + , LocalMapBase(customMarkers) , mHealth(NULL) , mMagicka(NULL) , mStamina(NULL) @@ -91,11 +95,12 @@ namespace MWGui , mSpellVisible(true) , mWorldMouseOver(false) , mEnemyHealthTimer(-1) + , mEnemyActorId(-1) , mIsDrowning(false) , mWeaponSpellTimer(0.f) , mDrowningFlashTheta(0.f) { - setCoord(0,0, width, height); + mMainWidget->setSize(MyGUI::RenderManager::getInstance().getViewSize()); // Energy bars getWidget(mHealthFrame, "HealthFrame"); @@ -157,7 +162,7 @@ namespace MWGui getWidget(mTriangleCounter, "TriangleCounter"); getWidget(mBatchCounter, "BatchCounter"); - LocalMapBase::init(mMinimap, mCompass, this); + LocalMapBase::init(mMinimap, mCompass); mMainWidget->eventMouseButtonClick += MyGUI::newDelegate(this, &HUD::onWorldClicked); mMainWidget->eventMouseMove += MyGUI::newDelegate(this, &HUD::onWorldMouseOver); @@ -383,6 +388,8 @@ namespace MWGui void HUD::onFrame(float dt) { + LocalMapBase::onFrame(dt); + mCellNameTimer -= dt; mWeaponSpellTimer -= dt; if (mCellNameTimer < 0) @@ -401,11 +408,6 @@ namespace MWGui mDrowningFlashTheta += dt * Ogre::Math::TWO_PI; } - void HUD::onResChange(int width, int height) - { - setCoord(0, 0, width, height); - } - void HUD::setSelectedSpell(const std::string& spellId, int successChancePercent) { const ESM::Spell* spell = @@ -423,9 +425,6 @@ namespace MWGui mSpellStatus->setProgressRange(100); mSpellStatus->setProgressPosition(successChancePercent); - if (mSpellImage->getChildCount()) - MyGUI::Gui::getInstance().destroyWidget(mSpellImage->getChildAt(0)); - mSpellBox->setUserString("ToolTipType", "Spell"); mSpellBox->setUserString("Spell", spellId); @@ -434,11 +433,12 @@ namespace MWGui MWBase::Environment::get().getWorld()->getStore().get().find(spell->mEffects.mList.front().mEffectID); std::string icon = effect->mIcon; - int slashPos = icon.find("\\"); + int slashPos = icon.rfind('\\'); icon.insert(slashPos+1, "b_"); - icon = std::string("icons\\") + icon; - Widgets::fixTexturePath(icon); - mSpellImage->setImageTexture(icon); + icon = Misc::ResourceHelpers::correctIconPath(icon); + + mSpellImage->setItem(MWWorld::Ptr()); + mSpellImage->setIcon(icon); } void HUD::setSelectedEnchantItem(const MWWorld::Ptr& item, int chargePercent) @@ -455,21 +455,10 @@ namespace MWGui mSpellStatus->setProgressRange(100); mSpellStatus->setProgressPosition(chargePercent); - if (mSpellImage->getChildCount()) - MyGUI::Gui::getInstance().destroyWidget(mSpellImage->getChildAt(0)); - mSpellBox->setUserString("ToolTipType", "ItemPtr"); mSpellBox->setUserData(item); - mSpellImage->setImageTexture("textures\\menu_icon_magic_mini.dds"); - MyGUI::ImageBox* itemBox = mSpellImage->createWidgetReal("ImageBox", MyGUI::FloatCoord(0,0,1,1) - , MyGUI::Align::Stretch); - - std::string path = std::string("icons\\"); - path+=item.getClass().getInventoryIcon(item); - Widgets::fixTexturePath(path); - itemBox->setImageTexture(path); - itemBox->setNeedMouseFocus(false); + mSpellImage->setItem(item); } void HUD::setSelectedWeapon(const MWWorld::Ptr& item, int durabilityPercent) @@ -483,29 +472,14 @@ namespace MWGui mWeaponSpellBox->setVisible(true); } + mWeapBox->clearUserStrings(); mWeapBox->setUserString("ToolTipType", "ItemPtr"); mWeapBox->setUserData(item); mWeapStatus->setProgressRange(100); mWeapStatus->setProgressPosition(durabilityPercent); - if (mWeapImage->getChildCount()) - MyGUI::Gui::getInstance().destroyWidget(mWeapImage->getChildAt(0)); - - std::string path = std::string("icons\\"); - path+=item.getClass().getInventoryIcon(item); - Widgets::fixTexturePath(path); - - if (item.getClass().getEnchantment(item) != "") - { - mWeapImage->setImageTexture("textures\\menu_icon_magic_mini.dds"); - MyGUI::ImageBox* itemBox = mWeapImage->createWidgetReal("ImageBox", MyGUI::FloatCoord(0,0,1,1) - , MyGUI::Align::Stretch); - itemBox->setImageTexture(path); - itemBox->setNeedMouseFocus(false); - } - else - mWeapImage->setImageTexture(path); + mWeapImage->setItem(item); } void HUD::unsetSelectedSpell() @@ -519,11 +493,9 @@ namespace MWGui mWeaponSpellBox->setVisible(true); } - if (mSpellImage->getChildCount()) - MyGUI::Gui::getInstance().destroyWidget(mSpellImage->getChildAt(0)); mSpellStatus->setProgressRange(100); mSpellStatus->setProgressPosition(0); - mSpellImage->setImageTexture(""); + mSpellImage->setItem(MWWorld::Ptr()); mSpellBox->clearUserStrings(); } @@ -538,19 +510,21 @@ namespace MWGui mWeaponSpellBox->setVisible(true); } - if (mWeapImage->getChildCount()) - MyGUI::Gui::getInstance().destroyWidget(mWeapImage->getChildAt(0)); mWeapStatus->setProgressRange(100); mWeapStatus->setProgressPosition(0); MWBase::World *world = MWBase::Environment::get().getWorld(); MWWorld::Ptr player = world->getPlayerPtr(); - if (player.getClass().getNpcStats(player).isWerewolf()) - mWeapImage->setImageTexture("icons\\k\\tx_werewolf_hand.dds"); - else - mWeapImage->setImageTexture("icons\\k\\stealth_handtohand.dds"); + + mWeapImage->setItem(MWWorld::Ptr()); + std::string icon = (player.getClass().getNpcStats(player).isWerewolf()) ? "icons\\k\\tx_werewolf_hand.dds" : "icons\\k\\stealth_handtohand.dds"; + mWeapImage->setIcon(icon); mWeapBox->clearUserStrings(); + mWeapBox->setUserString("ToolTipType", "Layout"); + mWeapBox->setUserString("ToolTipLayout", "HandToHandToolTip"); + mWeapBox->setUserString("Caption_HandToHandText", itemName); + mWeapBox->setUserString("ImageTexture_HandToHandImage", icon); } void HUD::setCrosshairVisible(bool visible) @@ -636,7 +610,10 @@ namespace MWGui void HUD::updateEnemyHealthBar() { - MWMechanics::CreatureStats& stats = mEnemy.getClass().getCreatureStats(mEnemy); + MWWorld::Ptr enemy = MWBase::Environment::get().getWorld()->searchPtrViaActorId(mEnemyActorId); + if (enemy.isEmpty()) + return; + MWMechanics::CreatureStats& stats = enemy.getClass().getCreatureStats(enemy); mEnemyHealth->setProgressRange(100); // Health is usually cast to int before displaying. Actors die whenever they are < 1 health. // Therefore any value < 1 should show as an empty health bar. We do the same in statswindow :) @@ -647,7 +624,7 @@ namespace MWGui { mSpellIcons->updateWidgets(mEffectBox, true); - if (!mEnemy.isEmpty() && mEnemyHealth->getVisible()) + if (mEnemyActorId != -1 && mEnemyHealth->getVisible()) { updateEnemyHealthBar(); } @@ -661,7 +638,7 @@ namespace MWGui void HUD::setEnemy(const MWWorld::Ptr &enemy) { - mEnemy = enemy; + mEnemyActorId = enemy.getClass().getCreatureStats(enemy).getActorId(); mEnemyHealthTimer = 5; if (!mEnemyHealth->getVisible()) mWeaponSpellBox->setPosition(mWeaponSpellBox->getPosition() - MyGUI::IntPoint(0,20)); @@ -671,7 +648,7 @@ namespace MWGui void HUD::resetEnemy() { - mEnemy = MWWorld::Ptr(); + mEnemyActorId = -1; mEnemyHealthTimer = -1; } diff --git a/apps/openmw/mwgui/hud.hpp b/apps/openmw/mwgui/hud.hpp index 973ac0745..ca68d907a 100644 --- a/apps/openmw/mwgui/hud.hpp +++ b/apps/openmw/mwgui/hud.hpp @@ -10,11 +10,12 @@ namespace MWGui { class DragAndDrop; class SpellIcons; + class ItemWidget; class HUD : public OEngine::GUI::Layout, public LocalMapBase { public: - HUD(int width, int height, int fpsLevel, DragAndDrop* dragAndDrop); + HUD(CustomMarkerCollection& customMarkers, int fpsLevel, DragAndDrop* dragAndDrop); virtual ~HUD(); void setValue (const std::string& id, const MWMechanics::DynamicStat& value); void setFPS(float fps); @@ -46,7 +47,6 @@ namespace MWGui void setCrosshairVisible(bool visible); void onFrame(float dt); - void onResChange(int width, int height); void setCellName(const std::string& cellName); @@ -63,7 +63,7 @@ namespace MWGui MyGUI::ProgressBar *mHealth, *mMagicka, *mStamina, *mEnemyHealth, *mDrowning; MyGUI::Widget* mHealthFrame; MyGUI::Widget *mWeapBox, *mSpellBox, *mSneakBox; - MyGUI::ImageBox *mWeapImage, *mSpellImage; + ItemWidget *mWeapImage, *mSpellImage; MyGUI::ProgressBar *mWeapStatus, *mSpellStatus; MyGUI::Widget *mEffectBox, *mMinimapBox; MyGUI::Button* mMinimapButton; @@ -74,8 +74,6 @@ namespace MWGui MyGUI::TextBox* mWeaponSpellBox; MyGUI::Widget *mDrowningFrame, *mDrowningFlash; - MyGUI::Widget* mDummy; - MyGUI::Widget* mFpsBox; MyGUI::TextBox* mFpsCounter; MyGUI::TextBox* mTriangleCounter; @@ -103,7 +101,7 @@ namespace MWGui SpellIcons* mSpellIcons; - MWWorld::Ptr mEnemy; + int mEnemyActorId; float mEnemyHealthTimer; bool mIsDrowning; diff --git a/apps/openmw/mwgui/inventoryitemmodel.cpp b/apps/openmw/mwgui/inventoryitemmodel.cpp index ad1a4e953..f45881770 100644 --- a/apps/openmw/mwgui/inventoryitemmodel.cpp +++ b/apps/openmw/mwgui/inventoryitemmodel.cpp @@ -61,6 +61,10 @@ void InventoryItemModel::removeItem (const ItemStack& item, size_t count) MWWorld::Ptr InventoryItemModel::moveItem(const ItemStack &item, size_t count, ItemModel *otherModel) { + // Can't move conjured items: This is a general fix that also takes care of issues with taking conjured items via the 'Take All' button. + if (item.mFlags & ItemStack::Flag_Bound) + return MWWorld::Ptr(); + bool setNewOwner = false; // Are you dead? Then you wont need that anymore diff --git a/apps/openmw/mwgui/inventorywindow.cpp b/apps/openmw/mwgui/inventorywindow.cpp index b1e8052d8..d1eeba157 100644 --- a/apps/openmw/mwgui/inventorywindow.cpp +++ b/apps/openmw/mwgui/inventorywindow.cpp @@ -37,11 +37,14 @@ namespace MWGui , mLastYSize(0) , mPreview(new MWRender::InventoryPreview(MWBase::Environment::get().getWorld ()->getPlayerPtr())) , mPreviewDirty(true) + , mPreviewResize(true) , mDragAndDrop(dragAndDrop) + , mSortModel(NULL) + , mTradeModel(NULL) , mSelectedItem(-1) , mGuiMode(GM_Inventory) { - static_cast(mMainWidget)->eventWindowChangeCoord += MyGUI::newDelegate(this, &InventoryWindow::onWindowResize); + mMainWidget->castType()->eventWindowChangeCoord += MyGUI::newDelegate(this, &InventoryWindow::onWindowResize); getWidget(mAvatar, "Avatar"); getWidget(mAvatarImage, "AvatarImage"); @@ -91,8 +94,18 @@ namespace MWGui mTradeModel = new TradeItemModel(new InventoryItemModel(mPtr), MWWorld::Ptr()); mSortModel = new SortFilterItemModel(mTradeModel); mItemView->setModel(mSortModel); + + mPreview.reset(NULL); + mAvatarImage->setImageTexture(""); + MyGUI::ITexture* tex = MyGUI::RenderManager::getInstance().getTexture("CharacterPreview"); + if (tex) + MyGUI::RenderManager::getInstance().destroyTexture(tex); + mPreview.reset(new MWRender::InventoryPreview(mPtr)); mPreview->setup(); + + mPreviewDirty = true; + mPreviewResize = true; } void InventoryWindow::setGuiMode(GuiMode mode) @@ -125,13 +138,18 @@ namespace MWGui Settings::Manager::getFloat(setting + " h", "Windows") * viewSize.height); if (size.width != mMainWidget->getWidth() || size.height != mMainWidget->getHeight()) - mPreviewDirty = true; + mPreviewResize = true; mMainWidget->setPosition(pos); mMainWidget->setSize(size); adjustPanes(); } + SortFilterItemModel* InventoryWindow::getSortFilterModel() + { + return mSortModel; + } + TradeItemModel* InventoryWindow::getTradeModel() { return mTradeModel; @@ -167,21 +185,20 @@ namespace MWGui MWWorld::Ptr object = item.mBase; int count = item.mCount; - // Bound items may not be moved - if (item.mBase.getCellRef().getRefId().size() > 6 - && item.mBase.getCellRef().getRefId().substr(0,6) == "bound_") - { - MWBase::Environment::get().getSoundManager()->playSound (sound, 1.0, 1.0); - MWBase::Environment::get().getWindowManager()->messageBox("#{sBarterDialog12}"); - return; - } - bool shift = MyGUI::InputManager::getInstance().isShiftPressed(); if (MyGUI::InputManager::getInstance().isControlPressed()) count = 1; if (mTrading) { + // Can't give conjured items to a merchant + if (item.mFlags & ItemStack::Flag_Bound) + { + MWBase::Environment::get().getSoundManager()->playSound (sound, 1.0, 1.0); + MWBase::Environment::get().getWindowManager()->messageBox("#{sBarterDialog9}"); + return; + } + // check if merchant accepts item int services = MWBase::Environment::get().getWindowManager()->getTradeWindow()->getMerchantServices(); if (!object.getClass().canSell(object, services)) @@ -213,9 +230,6 @@ namespace MWGui else dragItem (NULL, count); } - - // item might have been unequipped - notifyContentChanged(); } void InventoryWindow::ensureSelectedItemUnequipped() @@ -253,6 +267,7 @@ namespace MWGui { ensureSelectedItemUnequipped(); mDragAndDrop->startDrag(mSelectedItem, mSortModel, mTradeModel, mItemView, count); + notifyContentChanged(); } void InventoryWindow::sellItem(MyGUI::Widget* sender, int count) @@ -265,17 +280,18 @@ namespace MWGui if (item.mType == ItemStack::Type_Barter) { // this was an item borrowed to us by the merchant - MWBase::Environment::get().getWindowManager()->getTradeWindow()->returnItem(mSelectedItem, count); mTradeModel->returnItemBorrowedToUs(mSelectedItem, count); + MWBase::Environment::get().getWindowManager()->getTradeWindow()->returnItem(mSelectedItem, count); } else { // borrow item to the merchant - MWBase::Environment::get().getWindowManager()->getTradeWindow()->borrowItem(mSelectedItem, count); mTradeModel->borrowItemFromUs(mSelectedItem, count); + MWBase::Environment::get().getWindowManager()->getTradeWindow()->borrowItem(mSelectedItem, count); } mItemView->update(); + notifyContentChanged(); } void InventoryWindow::updateItemView() @@ -328,7 +344,7 @@ namespace MWGui { mLastXSize = mMainWidget->getSize().width; mLastYSize = mMainWidget->getSize().height; - mPreviewDirty = true; + mPreviewResize = true; } } @@ -353,7 +369,7 @@ namespace MWGui mItemView->update(); - static_cast(_sender)->setStateSelected(true); + _sender->castType()->setStateSelected(true); } void InventoryWindow::onPinToggled() @@ -361,6 +377,12 @@ namespace MWGui MWBase::Environment::get().getWindowManager()->setWeaponVisibility(!mPinned); } + void InventoryWindow::onTitleDoubleClicked() + { + if (!mPinned) + MWBase::Environment::get().getWindowManager()->toggleVisible(GW_Inventory); + } + void InventoryWindow::useItem(const MWWorld::Ptr &ptr) { const std::string& script = ptr.getClass().getScript(ptr); @@ -398,9 +420,13 @@ namespace MWGui else mSkippedToEquip = ptr; - mItemView->update(); + if (isVisible()) + { + mItemView->update(); - notifyContentChanged(); + notifyContentChanged(); + } + // else: will be updated in open() } void InventoryWindow::onAvatarClicked(MyGUI::Widget* _sender) @@ -468,6 +494,7 @@ namespace MWGui float capacity = player.getClass().getCapacity(player); float encumbrance = player.getClass().getEncumbrance(player); + mTradeModel->adjustEncumbrance(encumbrance); mEncumbranceBar->setValue(encumbrance, capacity); } @@ -486,16 +513,23 @@ namespace MWGui void InventoryWindow::doRenderUpdate () { - if (mPreviewDirty) + mPreview->onFrame(); + if (mPreviewResize) { - mPreviewDirty = false; + mPreviewResize = false; MyGUI::IntSize size = mAvatarImage->getSize(); - - mPreview->update (size.width, size.height); + mPreview->resize(size.width, size.height); mAvatarImage->setImageTexture("CharacterPreview"); mAvatarImage->setImageCoord(MyGUI::IntCoord(0, 0, std::min(512, size.width), std::min(1024, size.height))); mAvatarImage->setImageTile(MyGUI::IntSize(std::min(512, size.width), std::min(1024, size.height))); + } + if (mPreviewDirty) + { + mPreviewDirty = false; + mPreview->update (); + + mAvatarImage->setImageTexture("CharacterPreview"); mArmorRating->setCaptionWithReplacing ("#{sArmor}: " + boost::lexical_cast(static_cast(mPtr.getClass().getArmorRating(mPtr)))); @@ -510,6 +544,9 @@ namespace MWGui if (MWBase::Environment::get().getWindowManager()->getSpellWindow()) MWBase::Environment::get().getWindowManager()->getSpellWindow()->updateSpells(); + MWBase::Environment::get().getMechanicsManager()->updateMagicEffects( + MWBase::Environment::get().getWorld()->getPlayerPtr()); + mPreviewDirty = true; } diff --git a/apps/openmw/mwgui/inventorywindow.hpp b/apps/openmw/mwgui/inventorywindow.hpp index df563b3d4..6fd6ece4a 100644 --- a/apps/openmw/mwgui/inventorywindow.hpp +++ b/apps/openmw/mwgui/inventorywindow.hpp @@ -37,6 +37,7 @@ namespace MWGui mPreview->rebuild(); } + SortFilterItemModel* getSortFilterModel(); TradeItemModel* getTradeModel(); ItemModel* getModel(); @@ -52,6 +53,7 @@ namespace MWGui DragAndDrop* mDragAndDrop; bool mPreviewDirty; + bool mPreviewResize; int mSelectedItem; MWWorld::Ptr mPtr; @@ -97,6 +99,7 @@ namespace MWGui void onFilterChanged(MyGUI::Widget* _sender); void onAvatarClicked(MyGUI::Widget* _sender); void onPinToggled(); + void onTitleDoubleClicked(); void updateEncumbranceBar(); void notifyContentChanged(); diff --git a/apps/openmw/mwgui/itemmodel.cpp b/apps/openmw/mwgui/itemmodel.cpp index 55317724e..f5135ce80 100644 --- a/apps/openmw/mwgui/itemmodel.cpp +++ b/apps/openmw/mwgui/itemmodel.cpp @@ -2,6 +2,11 @@ #include "../mwworld/class.hpp" #include "../mwworld/containerstore.hpp" +#include "../mwworld/store.hpp" +#include "../mwworld/esmstore.hpp" + +#include "../mwbase/world.hpp" +#include "../mwbase/environment.hpp" namespace MWGui { @@ -15,6 +20,40 @@ namespace MWGui { if (base.getClass().getEnchantment(base) != "") mFlags |= Flag_Enchanted; + + static std::set boundItemIDCache; + + // If this is empty then we haven't executed the GMST cache logic yet; or there isn't any sMagicBound* GMST's for some reason + if (boundItemIDCache.empty()) + { + // Build a list of known bound item ID's + const MWWorld::Store &gameSettings = MWBase::Environment::get().getWorld()->getStore().get(); + + for (MWWorld::Store::iterator currentIteration = gameSettings.begin(); currentIteration != gameSettings.end(); ++currentIteration) + { + const ESM::GameSetting ¤tSetting = *currentIteration; + std::string currentGMSTID = currentSetting.mId; + Misc::StringUtils::toLower(currentGMSTID); + + // Don't bother checking this GMST if it's not a sMagicBound* one. + const std::string& toFind = "smagicbound"; + if (currentGMSTID.compare(0, toFind.length(), toFind) != 0) + continue; + + // All sMagicBound* GMST's should be of type string + std::string currentGMSTValue = currentSetting.getString(); + Misc::StringUtils::toLower(currentGMSTValue); + + boundItemIDCache.insert(currentGMSTValue); + } + } + + // Perform bound item check and assign the Flag_Bound bit if it passes + std::string tempItemID = base.getCellRef().getRefId(); + Misc::StringUtils::toLower(tempItemID); + + if (boundItemIDCache.count(tempItemID) != 0) + mFlags |= Flag_Bound; } ItemStack::ItemStack() @@ -100,7 +139,7 @@ namespace MWGui for (size_t i=0; igetItemCount(); ++i) { const ItemStack& item = mSourceModel->getItem(i); - if (item == itemToSearch) + if (item.mBase == itemToSearch.mBase) return i; } return -1; @@ -112,7 +151,7 @@ namespace MWGui for (size_t i=0; i EventHandle_Item; diff --git a/apps/openmw/mwgui/itemview.cpp b/apps/openmw/mwgui/itemview.cpp index 24bc3fd63..a51ada275 100644 --- a/apps/openmw/mwgui/itemview.cpp +++ b/apps/openmw/mwgui/itemview.cpp @@ -12,20 +12,11 @@ #include "../mwworld/class.hpp" #include "itemmodel.hpp" +#include "itemwidget.hpp" namespace MWGui { -std::string ItemView::getCountString(int count) -{ - if (count == 1) - return ""; - if (count > 9999) - return boost::lexical_cast(int(count/1000.f)) + "k"; - else - return boost::lexical_cast(count); -} - ItemView::ItemView() : mModel(NULL) , mScrollView(NULL) @@ -55,83 +46,22 @@ void ItemView::initialiseOverride() mScrollView->setCanvasAlign(MyGUI::Align::Left | MyGUI::Align::Top); } -void ItemView::update() +void ItemView::layoutWidgets() { - while (mScrollView->getChildCount()) - MyGUI::Gui::getInstance().destroyWidget(mScrollView->getChildAt(0)); - - if (!mModel) + if (!mScrollView->getChildCount()) return; int x = 0; int y = 0; int maxHeight = mScrollView->getSize().height - 58; - mModel->update(); - - MyGUI::Widget* dragArea = mScrollView->createWidget("",0,0,mScrollView->getWidth(),mScrollView->getHeight(), - MyGUI::Align::Stretch); - dragArea->setNeedMouseFocus(true); - dragArea->eventMouseButtonClick += MyGUI::newDelegate(this, &ItemView::onSelectedBackground); - dragArea->eventMouseWheel += MyGUI::newDelegate(this, &ItemView::onMouseWheel); + MyGUI::Widget* dragArea = mScrollView->getChildAt(0); - for (ItemModel::ModelIndex i=0; i(mModel->getItemCount()); ++i) + for (unsigned int i=0; igetChildCount(); ++i) { - const ItemStack& item = mModel->getItem(i); + MyGUI::Widget* w = dragArea->getChildAt(i); - /// \todo performance improvement: don't create/destroy all the widgets everytime the container window changes size, only reposition them - std::string path = std::string("icons\\"); - path += item.mBase.getClass().getInventoryIcon(item.mBase); - - // background widget (for the "equipped" frame and magic item background image) - bool isMagic = (item.mFlags & ItemStack::Flag_Enchanted); - MyGUI::ImageBox* backgroundWidget = dragArea->createWidget("ImageBox", - MyGUI::IntCoord(x, y, 42, 42), MyGUI::Align::Default); - backgroundWidget->setUserString("ToolTipType", "ItemModelIndex"); - backgroundWidget->setUserData(std::make_pair(i, mModel)); - - std::string backgroundTex = "textures\\menu_icon"; - if (isMagic) - backgroundTex += "_magic"; - if (item.mType == ItemStack::Type_Normal) - { - if (!isMagic) - backgroundTex = ""; - } - else if (item.mType == ItemStack::Type_Equipped) - backgroundTex += "_equip"; - else if (item.mType == ItemStack::Type_Barter) - backgroundTex += "_barter"; - - if (backgroundTex != "") - backgroundTex += ".dds"; - - backgroundWidget->setImageTexture(backgroundTex); - if ((item.mType == ItemStack::Type_Barter) && !isMagic) - backgroundWidget->setProperty("ImageCoord", "2 2 42 42"); - else - backgroundWidget->setProperty("ImageCoord", "0 0 42 42"); - backgroundWidget->eventMouseButtonClick += MyGUI::newDelegate(this, &ItemView::onSelectedItem); - backgroundWidget->eventMouseWheel += MyGUI::newDelegate(this, &ItemView::onMouseWheel); - - // image - MyGUI::ImageBox* image = backgroundWidget->createWidget("ImageBox", - MyGUI::IntCoord(5, 5, 32, 32), MyGUI::Align::Default); - std::string::size_type pos = path.rfind("."); - if(pos != std::string::npos) - path.erase(pos); - path.append(".dds"); - image->setImageTexture(path); - image->setNeedMouseFocus(false); - - // text widget that shows item count - MyGUI::TextBox* text = image->createWidget("SandBrightText", - MyGUI::IntCoord(0, 14, 32, 18), MyGUI::Align::Default, std::string("Label")); - text->setTextAlign(MyGUI::Align::Right); - text->setNeedMouseFocus(false); - text->setTextShadow(true); - text->setTextShadowColour(MyGUI::Colour(0,0,0)); - text->setCaption(getCountString(item.mCount)); + w->setPosition(x, y); y += 42; if (y > maxHeight) @@ -139,14 +69,59 @@ void ItemView::update() x += 42; y = 0; } - } x += 42; + MyGUI::IntSize size = MyGUI::IntSize(std::max(mScrollView->getSize().width, x), mScrollView->getSize().height); + + // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the scrollbar is hidden + mScrollView->setVisibleVScroll(false); + mScrollView->setVisibleHScroll(false); mScrollView->setCanvasSize(size); + mScrollView->setVisibleVScroll(true); + mScrollView->setVisibleHScroll(true); dragArea->setSize(size); } +void ItemView::update() +{ + while (mScrollView->getChildCount()) + MyGUI::Gui::getInstance().destroyWidget(mScrollView->getChildAt(0)); + + if (!mModel) + return; + + mModel->update(); + + MyGUI::Widget* dragArea = mScrollView->createWidget("",0,0,mScrollView->getWidth(),mScrollView->getHeight(), + MyGUI::Align::Stretch); + dragArea->setNeedMouseFocus(true); + dragArea->eventMouseButtonClick += MyGUI::newDelegate(this, &ItemView::onSelectedBackground); + dragArea->eventMouseWheel += MyGUI::newDelegate(this, &ItemView::onMouseWheel); + + for (ItemModel::ModelIndex i=0; i(mModel->getItemCount()); ++i) + { + const ItemStack& item = mModel->getItem(i); + + ItemWidget* itemWidget = dragArea->createWidget("MW_ItemIcon", + MyGUI::IntCoord(0, 0, 42, 42), MyGUI::Align::Default); + itemWidget->setUserString("ToolTipType", "ItemModelIndex"); + itemWidget->setUserData(std::make_pair(i, mModel)); + ItemWidget::ItemState state = ItemWidget::None; + if (item.mType == ItemStack::Type_Barter) + state = ItemWidget::Barter; + if (item.mType == ItemStack::Type_Equipped) + state = ItemWidget::Equip; + itemWidget->setItem(item.mBase, state); + itemWidget->setCount(item.mCount); + + itemWidget->eventMouseButtonClick += MyGUI::newDelegate(this, &ItemView::onSelectedItem); + itemWidget->eventMouseWheel += MyGUI::newDelegate(this, &ItemView::onMouseWheel); + } + + layoutWidgets(); +} + void ItemView::onSelectedItem(MyGUI::Widget *sender) { ItemModel::ModelIndex index = (*sender->getUserData >()).first; @@ -168,26 +143,28 @@ void ItemView::onMouseWheel(MyGUI::Widget *_sender, int _rel) void ItemView::setSize(const MyGUI::IntSize &_value) { + bool changed = (_value.width != getWidth() || _value.height != getHeight()); Base::setSize(_value); - update(); + if (changed) + layoutWidgets(); } void ItemView::setSize(int _width, int _height) { - Base::setSize(_width, _height); - update(); + setSize(MyGUI::IntSize(_width, _height)); } void ItemView::setCoord(const MyGUI::IntCoord &_value) { + bool changed = (_value.width != getWidth() || _value.height != getHeight()); Base::setCoord(_value); - update(); + if (changed) + layoutWidgets(); } void ItemView::setCoord(int _left, int _top, int _width, int _height) { - Base::setCoord(_left, _top, _width, _height); - update(); + setCoord(MyGUI::IntCoord(_left, _top, _width, _height)); } void ItemView::registerComponents() diff --git a/apps/openmw/mwgui/itemview.hpp b/apps/openmw/mwgui/itemview.hpp index 74bc66ea0..1a5bd79a3 100644 --- a/apps/openmw/mwgui/itemview.hpp +++ b/apps/openmw/mwgui/itemview.hpp @@ -30,11 +30,11 @@ namespace MWGui void update(); - static std::string getCountString(int count); - private: virtual void initialiseOverride(); + void layoutWidgets(); + virtual void setSize(const MyGUI::IntSize& _value); virtual void setCoord(const MyGUI::IntCoord& _value); void setSize(int _width, int _height); diff --git a/apps/openmw/mwgui/itemwidget.cpp b/apps/openmw/mwgui/itemwidget.cpp new file mode 100644 index 000000000..b6a288078 --- /dev/null +++ b/apps/openmw/mwgui/itemwidget.cpp @@ -0,0 +1,128 @@ +#include "itemwidget.hpp" + +#include +#include +#include + +#include + +#include + +#include "../mwworld/class.hpp" + +namespace +{ + std::string getCountString(int count) + { + if (count == 1) + return ""; + if (count > 9999) + return boost::lexical_cast(int(count/1000.f)) + "k"; + else + return boost::lexical_cast(count); + } +} + +namespace MWGui +{ + + ItemWidget::ItemWidget() + : mItem(NULL) + , mFrame(NULL) + , mText(NULL) + { + + } + + void ItemWidget::registerComponents() + { + MyGUI::FactoryManager::getInstance().registerFactory("Widget"); + } + + void ItemWidget::initialiseOverride() + { + assignWidget(mItem, "Item"); + if (mItem) + mItem->setNeedMouseFocus(false); + assignWidget(mFrame, "Frame"); + if (mFrame) + mFrame->setNeedMouseFocus(false); + assignWidget(mText, "Text"); + if (mText) + mText->setNeedMouseFocus(false); + + Base::initialiseOverride(); + } + + void ItemWidget::setCount(int count) + { + if (!mText) + return; + mText->setCaption(getCountString(count)); + } + + void ItemWidget::setIcon(const std::string &icon) + { + if (mItem) + mItem->setImageTexture(icon); + } + + void ItemWidget::setFrame(const std::string &frame, const MyGUI::IntCoord &coord) + { + if (mFrame) + { + mFrame->setImageTexture(frame); + mFrame->setImageTile(MyGUI::IntSize(coord.width, coord.height)); // Why is this needed? MyGUI bug? + mFrame->setImageCoord(coord); + } + } + + void ItemWidget::setIcon(const MWWorld::Ptr &ptr) + { + setIcon(Misc::ResourceHelpers::correctIconPath(ptr.getClass().getInventoryIcon(ptr))); + } + + + void ItemWidget::setItem(const MWWorld::Ptr &ptr, ItemState state) + { + if (!mItem) + return; + + if (ptr.isEmpty()) + { + if (mFrame) + mFrame->setImageTexture(""); + mItem->setImageTexture(""); + mText->setCaption(""); + return; + } + + bool isMagic = !ptr.getClass().getEnchantment(ptr).empty(); + + std::string backgroundTex = "textures\\menu_icon"; + if (isMagic) + backgroundTex += "_magic"; + if (state == None) + { + if (!isMagic) + backgroundTex = ""; + } + else if (state == Equip) + { + backgroundTex += "_equip"; + } + else if (state == Barter) + backgroundTex += "_barter"; + + if (backgroundTex != "") + backgroundTex += ".dds"; + + if (state == Barter && !isMagic) + setFrame(backgroundTex, MyGUI::IntCoord(2,2,42,42)); + else + setFrame(backgroundTex, MyGUI::IntCoord(0,0,42,42)); + + setIcon(ptr); + } + +} diff --git a/apps/openmw/mwgui/itemwidget.hpp b/apps/openmw/mwgui/itemwidget.hpp new file mode 100644 index 000000000..e7a902239 --- /dev/null +++ b/apps/openmw/mwgui/itemwidget.hpp @@ -0,0 +1,53 @@ +#ifndef OPENMW_MWGUI_ITEMWIDGET_H +#define OPENMW_MWGUI_ITEMWIDGET_H + +#include + +namespace MWWorld +{ + class Ptr; +} + +namespace MWGui +{ + + /// @brief A widget that shows an icon for an MWWorld::Ptr + class ItemWidget : public MyGUI::Widget + { + MYGUI_RTTI_DERIVED(ItemWidget) + public: + ItemWidget(); + + /// Register needed components with MyGUI's factory manager + static void registerComponents (); + + enum ItemState + { + None, + Equip, + Barter, + Magic + }; + + /// Set count to be displayed in a textbox over the item + void setCount(int count); + + /// \a ptr may be empty + void setItem (const MWWorld::Ptr& ptr, ItemState state = None); + + // Set icon and frame manually + void setIcon (const std::string& icon); + void setIcon (const MWWorld::Ptr& ptr); + void setFrame (const std::string& frame, const MyGUI::IntCoord& coord); + + private: + virtual void initialiseOverride(); + + MyGUI::ImageBox* mItem; + MyGUI::ImageBox* mFrame; + MyGUI::TextBox* mText; + }; + +} + +#endif diff --git a/apps/openmw/mwgui/journalbooks.cpp b/apps/openmw/mwgui/journalbooks.cpp index 683fe9208..34a852562 100644 --- a/apps/openmw/mwgui/journalbooks.cpp +++ b/apps/openmw/mwgui/journalbooks.cpp @@ -1,10 +1,13 @@ #include "journalbooks.hpp" +#include + namespace { - const MyGUI::Colour linkHot (0.40f, 0.40f, 0.80f); - const MyGUI::Colour linkNormal (0.20f, 0.20f, 0.60f); - const MyGUI::Colour linkActive (0.50f, 0.50f, 1.00f); + MyGUI::Colour getTextColour (const std::string& type) + { + return MyGUI::Colour::parse(MyGUI::LanguageManager::getInstance().replaceTags("#{fontcolour=" + type + "}")); + } struct AddContent { @@ -28,6 +31,10 @@ namespace { MWGui::BookTypesetter::Style* style = mBodyStyle; + static const MyGUI::Colour linkHot (getTextColour("journal_link_over")); + static const MyGUI::Colour linkNormal (getTextColour("journal_link")); + static const MyGUI::Colour linkActive (getTextColour("journal_link_pressed")); + if (topicId) style = mTypesetter->createHotStyle (mBodyStyle, linkNormal, linkHot, linkActive, topicId); @@ -132,38 +139,6 @@ namespace mTypesetter->sectionBreak (10); } }; - - struct AddTopicLink : AddContent - { - AddTopicLink (MWGui::BookTypesetter::Ptr typesetter, MWGui::BookTypesetter::Style* style) : - AddContent (typesetter, style) - { - } - - void operator () (MWGui::JournalViewModel::TopicId topicId, MWGui::JournalViewModel::Utf8Span name) - { - MWGui::BookTypesetter::Style* link = mTypesetter->createHotStyle (mBodyStyle, MyGUI::Colour::Black, linkHot, linkActive, topicId); - - mTypesetter->write (link, name); - mTypesetter->lineBreak (); - } - }; - - struct AddQuestLink : AddContent - { - AddQuestLink (MWGui::BookTypesetter::Ptr typesetter, MWGui::BookTypesetter::Style* style) : - AddContent (typesetter, style) - { - } - - void operator () (MWGui::JournalViewModel::QuestId id, MWGui::JournalViewModel::Utf8Span name) - { - MWGui::BookTypesetter::Style* style = mTypesetter->createHotStyle (mBodyStyle, MyGUI::Colour::Black, linkHot, linkActive, id); - - mTypesetter->write (style, name); - mTypesetter->lineBreak (); - } - }; } namespace MWGui @@ -206,7 +181,7 @@ book JournalBooks::createJournalBook () BookTypesetter::Style* header = typesetter->createStyle ("", MyGUI::Colour (0.60f, 0.00f, 0.00f)); BookTypesetter::Style* body = typesetter->createStyle ("", MyGUI::Colour::Black); - mModel->visitJournalEntries (0, AddJournalEntry (typesetter, body, header, true)); + mModel->visitJournalEntries ("", AddJournalEntry (typesetter, body, header, true)); return typesetter->complete (); } @@ -227,16 +202,17 @@ book JournalBooks::createTopicBook (uintptr_t topicId) return typesetter->complete (); } -book JournalBooks::createQuestBook (uintptr_t questId) +book JournalBooks::createQuestBook (const std::string& questName) { BookTypesetter::Ptr typesetter = createTypesetter (); BookTypesetter::Style* header = typesetter->createStyle ("", MyGUI::Colour (0.60f, 0.00f, 0.00f)); BookTypesetter::Style* body = typesetter->createStyle ("", MyGUI::Colour::Black); - mModel->visitQuestName (questId, AddQuestName (typesetter, header)); + AddQuestName addName (typesetter, header); + addName(to_utf8_span(questName.c_str())); - mModel->visitJournalEntries (questId, AddJournalEntry (typesetter, body, header, false)); + mModel->visitJournalEntries (questName, AddJournalEntry (typesetter, body, header, false)); return typesetter->complete (); } @@ -257,7 +233,11 @@ book JournalBooks::createTopicIndexBook () sprintf (buffer, "( %c )", ch); - BookTypesetter::Style* style = typesetter->createHotStyle (body, MyGUI::Colour::Black, linkHot, linkActive, ch); + MyGUI::Colour linkHot (getTextColour("journal_topic_over")); + MyGUI::Colour linkActive (getTextColour("journal_topic_pressed")); + MyGUI::Colour linkNormal (getTextColour("journal_topic")); + + BookTypesetter::Style* style = typesetter->createHotStyle (body, linkNormal, linkHot, linkActive, ch); if (i == 13) typesetter->sectionBreak (); @@ -269,26 +249,6 @@ book JournalBooks::createTopicIndexBook () return typesetter->complete (); } -book JournalBooks::createTopicIndexBook (char character) -{ - BookTypesetter::Ptr typesetter = BookTypesetter::create (0x7FFFFFFF, 0x7FFFFFFF); - BookTypesetter::Style* style = typesetter->createStyle ("", MyGUI::Colour::Black); - - mModel->visitTopicNamesStartingWith (character, AddTopicLink (typesetter, style)); - - return typesetter->complete (); -} - -book JournalBooks::createQuestIndexBook (bool activeOnly) -{ - BookTypesetter::Ptr typesetter = BookTypesetter::create (0x7FFFFFFF, 0x7FFFFFFF); - BookTypesetter::Style* base = typesetter->createStyle ("", MyGUI::Colour::Black); - - mModel->visitQuestNames (activeOnly, AddQuestLink (typesetter, base)); - - return typesetter->complete (); -} - BookTypesetter::Ptr JournalBooks::createTypesetter () { //TODO: determine page size from layout... diff --git a/apps/openmw/mwgui/journalbooks.hpp b/apps/openmw/mwgui/journalbooks.hpp index 819bda0fd..8f87825f0 100644 --- a/apps/openmw/mwgui/journalbooks.hpp +++ b/apps/openmw/mwgui/journalbooks.hpp @@ -18,10 +18,9 @@ namespace MWGui Book createEmptyJournalBook (); Book createJournalBook (); Book createTopicBook (uintptr_t topicId); - Book createQuestBook (uintptr_t questId); + Book createTopicBook (const std::string& topicId); + Book createQuestBook (const std::string& questName); Book createTopicIndexBook (); - Book createTopicIndexBook (char character); - Book createQuestIndexBook (bool showAll); private: BookTypesetter::Ptr createTypesetter (); diff --git a/apps/openmw/mwgui/journalviewmodel.cpp b/apps/openmw/mwgui/journalviewmodel.cpp index 5994c6e21..059af463f 100644 --- a/apps/openmw/mwgui/journalviewmodel.cpp +++ b/apps/openmw/mwgui/journalviewmodel.cpp @@ -12,9 +12,9 @@ #include "../mwbase/journal.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" -#include "../mwdialogue/journalentry.hpp" -#include "keywordsearch.hpp" +#include "../mwdialogue/journalentry.hpp" +#include "../mwdialogue/keywordsearch.hpp" namespace MWGui { @@ -22,7 +22,7 @@ struct JournalViewModelImpl; struct JournalViewModelImpl : JournalViewModel { - typedef KeywordSearch KeywordSearchT; + typedef MWDialogue::KeywordSearch KeywordSearchT; mutable bool mKeywordSearchLoaded; mutable KeywordSearchT mKeywordSearch; @@ -113,10 +113,10 @@ struct JournalViewModelImpl : JournalViewModel utf8text = getText (); - size_t pos_begin, pos_end; + size_t pos_end = 0; for(;;) { - pos_begin = utf8text.find('@'); + size_t pos_begin = utf8text.find('@'); if (pos_begin != std::string::npos) pos_end = utf8text.find('#', pos_begin); @@ -195,31 +195,43 @@ struct JournalViewModelImpl : JournalViewModel }; - void visitQuestNames (bool active_only, boost::function visitor) const + void visitQuestNames (bool active_only, boost::function visitor) const { MWBase::Journal * journal = MWBase::Environment::get ().getJournal (); + std::set visitedQuests; + + // Note that for purposes of the journal GUI, quests are identified by the name, not the ID, so several + // different quest IDs can end up in the same quest log. A quest log should be considered finished + // when any quest ID in that log is finished. for (MWBase::Journal::TQuestIter i = journal->questBegin (); i != journal->questEnd (); ++i) { - if (active_only && i->second.isFinished ()) + const MWDialogue::Quest& quest = i->second; + + bool isFinished = false; + for (MWBase::Journal::TQuestIter j = journal->questBegin (); j != journal->questEnd (); ++j) + { + if (quest.getName() == j->second.getName() && j->second.isFinished()) + isFinished = true; + } + + if (active_only && isFinished) continue; - const MWDialogue::Quest& quest = i->second; // Unfortunately Morrowind.esm has no quest names, since the quest book was added with tribunal. // Note that even with Tribunal, some quests still don't have quest names. I'm assuming those are not supposed // to appear in the quest book. if (!quest.getName().empty()) - visitor (reinterpret_cast (&i->second), toUtf8Span (quest.getName())); - } - } - - void visitQuestName (QuestId questId, boost::function visitor) const - { - MWDialogue::Quest const * quest = reinterpret_cast (questId); + { + // Don't list the same quest name twice + if (visitedQuests.find(quest.getName()) != visitedQuests.end()) + continue; - std::string name = quest->getName (); + visitor (quest.getName()); - visitor (toUtf8Span (name)); + visitedQuests.insert(quest.getName()); + } + } } template @@ -249,7 +261,7 @@ struct JournalViewModelImpl : JournalViewModel os << itr->mDayOfMonth << ' ' << MWBase::Environment::get().getWorld()->getMonthName (itr->mMonth) - << " (" << dayStr << " " << (itr->mDay + 1) << ')'; + << " (" << dayStr << " " << (itr->mDay) << ')'; timestamp_buffer = os.str (); } @@ -258,20 +270,29 @@ struct JournalViewModelImpl : JournalViewModel } }; - void visitJournalEntries (QuestId questId, boost::function visitor) const + void visitJournalEntries (const std::string& questName, boost::function visitor) const { MWBase::Journal * journal = MWBase::Environment::get().getJournal(); - if (questId != 0) + if (!questName.empty()) { - MWDialogue::Quest const * quest = reinterpret_cast (questId); + std::vector quests; + for (MWBase::Journal::TQuestIter questIt = journal->questBegin(); questIt != journal->questEnd(); ++questIt) + { + if (Misc::StringUtils::ciEqual(questIt->second.getName(), questName)) + quests.push_back(&questIt->second); + } for(MWBase::Journal::TEntryIter i = journal->begin(); i != journal->end (); ++i) { - for (MWDialogue::Topic::TEntryIter j = quest->begin (); j != quest->end (); ++j) + for (std::vector::iterator questIt = quests.begin(); questIt != quests.end(); ++questIt) { - if (i->mInfoId == j->mInfoId) - visitor (JournalEntryImpl (this, i)); + MWDialogue::Quest const* quest = *questIt; + for (MWDialogue::Topic::TEntryIter j = quest->begin (); j != quest->end (); ++j) + { + if (i->mInfoId == j->mInfoId) + visitor (JournalEntryImpl (this, i)); + } } } } @@ -282,18 +303,13 @@ struct JournalViewModelImpl : JournalViewModel } } - void visitTopics (boost::function visitor) const - { - throw std::runtime_error ("not implemented"); - } - void visitTopicName (TopicId topicId, boost::function visitor) const { MWDialogue::Topic const & topic = * reinterpret_cast (topicId); visitor (toUtf8Span (topic.getName())); } - void visitTopicNamesStartingWith (char character, boost::function < void (TopicId , Utf8Span) > visitor) const + void visitTopicNamesStartingWith (char character, boost::function < void (const std::string&) > visitor) const { MWBase::Journal * journal = MWBase::Environment::get().getJournal(); @@ -302,7 +318,7 @@ struct JournalViewModelImpl : JournalViewModel if (i->first [0] != std::tolower (character, mLocale)) continue; - visitor (TopicId (&i->second), toUtf8Span (i->second.getName())); + visitor (i->second.getName()); } } diff --git a/apps/openmw/mwgui/journalviewmodel.hpp b/apps/openmw/mwgui/journalviewmodel.hpp index 9efdeae54..5f0189b59 100644 --- a/apps/openmw/mwgui/journalviewmodel.hpp +++ b/apps/openmw/mwgui/journalviewmodel.hpp @@ -67,20 +67,18 @@ namespace MWGui /// returns true if their are no journal entries to display virtual bool isEmpty () const = 0; - /// provides access to the name of the quest with the specified identifier - virtual void visitQuestName (TopicId topicId, boost::function visitor) const = 0; + /// walks the active and optionally completed, quests providing the name + virtual void visitQuestNames (bool active_only, boost::function visitor) const = 0; - /// walks the active and optionally completed, quests providing the quest id and name - virtual void visitQuestNames (bool active_only, boost::function visitor) const = 0; - - /// walks over the journal entries related to the specified quest identified by its id - virtual void visitJournalEntries (QuestId questId, boost::function visitor) const = 0; + /// walks over the journal entries related to all quests with the given name + /// If \a questName is empty, simply visits all journal entries + virtual void visitJournalEntries (const std::string& questName, boost::function visitor) const = 0; /// provides the name of the topic specified by its id virtual void visitTopicName (TopicId topicId, boost::function visitor) const = 0; - /// walks over the topics whose names start with the specified character providing the topics id and name - virtual void visitTopicNamesStartingWith (char character, boost::function < void (TopicId , Utf8Span) > visitor) const = 0; + /// walks over the topics whose names start with the specified character providing the topics name + virtual void visitTopicNamesStartingWith (char character, boost::function < void (const std::string&) > visitor) const = 0; /// walks over the topic entries for the topic specified by its identifier virtual void visitTopicEntries (TopicId topicId, boost::function visitor) const = 0; diff --git a/apps/openmw/mwgui/journalwindow.cpp b/apps/openmw/mwgui/journalwindow.cpp index f3c9e9c73..0bcd5062d 100644 --- a/apps/openmw/mwgui/journalwindow.cpp +++ b/apps/openmw/mwgui/journalwindow.cpp @@ -3,7 +3,7 @@ #include "../mwbase/environment.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwbase/windowmanager.hpp" -#include "list.hpp" +#include "../mwbase/journal.hpp" #include #include @@ -14,9 +14,11 @@ #include #include "boost/lexical_cast.hpp" +#include +#include + #include "bookpage.hpp" #include "windowbase.hpp" -#include "imagebutton.hpp" #include "journalviewmodel.hpp" #include "journalbooks.hpp" @@ -36,9 +38,7 @@ namespace static char const PageOneNum [] = "PageOneNum"; static char const PageTwoNum [] = "PageTwoNum"; static char const TopicsList [] = "TopicsList"; - static char const TopicsPage [] = "TopicsPage"; static char const QuestsList [] = "QuestsList"; - static char const QuestsPage [] = "QuestsPage"; static char const LeftBookPage [] = "LeftBookPage"; static char const RightBookPage [] = "RightBookPage"; static char const LeftTopicIndex [] = "LeftTopicIndex"; @@ -82,7 +82,7 @@ namespace void adviseButtonClick (char const * name, void (JournalWindowImpl::*Handler) (MyGUI::Widget* _sender)) { - getWidget (name) -> + getWidget (name) -> eventMouseButtonClick += newDelegate(this, Handler); } @@ -110,14 +110,22 @@ namespace adviseButtonClick (ShowAllBTN, &JournalWindowImpl::notifyShowAll ); adviseButtonClick (ShowActiveBTN, &JournalWindowImpl::notifyShowActive); + Gui::MWList* list = getWidget(QuestsList); + list->eventItemSelected += MyGUI::newDelegate(this, &JournalWindowImpl::notifyQuestClicked); + + Gui::MWList* topicsList = getWidget(TopicsList); + topicsList->eventItemSelected += MyGUI::newDelegate(this, &JournalWindowImpl::notifyTopicSelected); + { MWGui::BookPage::ClickCallback callback; callback = boost::bind (&JournalWindowImpl::notifyTopicClicked, this, _1); - getPage (TopicsPage)->adviseLinkClicked (callback); getPage (LeftBookPage)->adviseLinkClicked (callback); getPage (RightBookPage)->adviseLinkClicked (callback); + + getPage (LeftBookPage)->eventMouseWheel += MyGUI::newDelegate(this, &JournalWindowImpl::notifyMouseWheel); + getPage (RightBookPage)->eventMouseWheel += MyGUI::newDelegate(this, &JournalWindowImpl::notifyMouseWheel); } { @@ -129,14 +137,6 @@ namespace getPage (RightTopicIndex)->adviseLinkClicked (callback); } - { - MWGui::BookPage::ClickCallback callback; - - callback = boost::bind (&JournalWindowImpl::notifyQuestClicked, this, _1); - - getPage (QuestsPage)->adviseLinkClicked (callback); - } - adjustButton(OptionsBTN, true); adjustButton(PrevPageBTN); adjustButton(NextPageBTN); @@ -146,12 +146,12 @@ namespace adjustButton(ShowActiveBTN, true); adjustButton(JournalBTN); - MWGui::ImageButton* optionsButton = getWidget(OptionsBTN); + Gui::ImageButton* optionsButton = getWidget(OptionsBTN); if (optionsButton->getWidth() == 0) { // If tribunal is not installed (-> no options button), we still want the Topics button available, // so place it where the options button would have been - MWGui::ImageButton* topicsButton = getWidget(TopicsBTN); + Gui::ImageButton* topicsButton = getWidget(TopicsBTN); topicsButton->detachFromWidget(); topicsButton->attachToWidget(optionsButton->getParent()); topicsButton->setPosition(optionsButton->getPosition()); @@ -159,7 +159,7 @@ namespace topicsButton->eventMouseButtonClick += MyGUI::newDelegate(this, &JournalWindowImpl::notifyOptions); } - MWGui::ImageButton* nextButton = getWidget(NextPageBTN); + Gui::ImageButton* nextButton = getWidget(NextPageBTN); if (nextButton->getSize().width == 64) { // english button has a 7 pixel wide strip of garbage on its right edge @@ -182,7 +182,7 @@ namespace void adjustButton (char const * name, bool optional = false) { - MWGui::ImageButton* button = getWidget(name); + Gui::ImageButton* button = getWidget(name); MyGUI::IntSize diff = button->getSize() - button->getRequestedSize(!optional); button->setSize(button->getRequestedSize(!optional)); @@ -201,10 +201,6 @@ namespace setBookMode (); - /// \todo Wiping the whole book layout each time the journal is opened is probably too costly for a large journal (eg 300+ pages). - /// There should be a way to keep the existing layout and append new entries to the end of it. - /// However, that still leaves the problem of having to add links to previously unknown, but now known topics, so - /// we maybe need to find another way to speed things up. Book journalBook; if (mModel->isEmpty ()) journalBook = createEmptyJournalBook (); @@ -271,6 +267,10 @@ namespace //TODO: figure out how to make "options" page overlay book page // correctly, so that text may show underneath getPage (RightBookPage)->showPage (Book (), 0); + + // If in quest mode, ensure the quest list is updated + if (mQuestMode) + notifyQuests(getWidget(QuestsList)); } void pushBook (Book book, unsigned int page) @@ -349,9 +349,22 @@ namespace setVisible (JournalBTN, true); } - void notifyQuestClicked (intptr_t questId) + void notifyTopicSelected (const std::string& topic, int id) { - Book book = createQuestBook (questId); + const MWBase::Journal* journal = MWBase::Environment::get().getJournal(); + intptr_t topicId = 0; /// \todo get rid of intptr ids + for(MWBase::Journal::TTopicIter i = journal->topicBegin(); i != journal->topicEnd (); ++i) + { + if (Misc::StringUtils::ciEqual(i->first, topic)) + topicId = intptr_t (&i->second); + } + + notifyTopicClicked(topicId); + } + + void notifyQuestClicked (const std::string& name, int id) + { + Book book = createQuestBook (name); if (mStates.size () > 1) replaceBook (book, 0); @@ -380,22 +393,20 @@ namespace popBook (); } - void showList (char const * listId, char const * pageId, Book book) - { - std::pair size = book->getSize (); - - getPage (pageId)->showPage (book, 0); - - getWidget (listId)->setCanvasSize (size.first, size.second); - } - void notifyIndexLinkClicked (MWGui::TypesetBook::InteractiveId character) { setVisible (LeftTopicIndex, false); setVisible (RightTopicIndex, false); setVisible (TopicsList, true); - showList (TopicsList, TopicsPage, createTopicIndexBook ((char)character)); + Gui::MWList* list = getWidget(TopicsList); + list->clear(); + + AddNamesToList add(list); + + mModel->visitTopicNamesStartingWith((char) character, add); + + list->adjustSize(); } void notifyTopics(MyGUI::Widget* _sender) @@ -409,9 +420,21 @@ namespace setVisible (ShowActiveBTN, false); } + struct AddNamesToList + { + AddNamesToList(Gui::MWList* list) : mList(list) {} + + Gui::MWList* mList; + void operator () (const std::string& name) + { + mList->addItem(name); + } + }; + void notifyQuests(MyGUI::Widget* _sender) { mQuestMode = true; + setVisible (LeftTopicIndex, false); setVisible (RightTopicIndex, false); setVisible (TopicsList, false); @@ -419,23 +442,26 @@ namespace setVisible (ShowAllBTN, !mAllQuests); setVisible (ShowActiveBTN, mAllQuests); - showList (QuestsList, QuestsPage, createQuestIndexBook (!mAllQuests)); + Gui::MWList* list = getWidget(QuestsList); + list->clear(); + + AddNamesToList add(list); + + mModel->visitQuestNames(!mAllQuests, add); + + list->adjustSize(); } void notifyShowAll(MyGUI::Widget* _sender) { mAllQuests = true; - setVisible (ShowAllBTN, !mAllQuests); - setVisible (ShowActiveBTN, mAllQuests); - showList (QuestsList, QuestsPage, createQuestIndexBook (!mAllQuests)); + notifyQuests(_sender); } void notifyShowActive(MyGUI::Widget* _sender) { mAllQuests = false; - setVisible (ShowAllBTN, !mAllQuests); - setVisible (ShowActiveBTN, mAllQuests); - showList (QuestsList, QuestsPage, createQuestIndexBook (!mAllQuests)); + notifyQuests(_sender); } void notifyCancel(MyGUI::Widget* _sender) @@ -449,6 +475,14 @@ namespace MWBase::Environment::get().getWindowManager ()->popGuiMode (); } + void notifyMouseWheel(MyGUI::Widget* sender, int rel) + { + if (rel < 0) + notifyNextPage(sender); + else + notifyPrevPage(sender); + } + void notifyNextPage(MyGUI::Widget* _sender) { if (!mStates.empty ()) @@ -456,7 +490,7 @@ namespace unsigned int & page = mStates.top ().mPage; Book book = mStates.top ().mBook; - if (page < book->pageCount () - 2) + if (page+2 < book->pageCount()) { page += 2; updateShowingPages (); @@ -470,7 +504,7 @@ namespace { unsigned int & page = mStates.top ().mPage; - if(page > 0) + if(page >= 2) { page -= 2; updateShowingPages (); diff --git a/apps/openmw/mwgui/levelupdialog.cpp b/apps/openmw/mwgui/levelupdialog.cpp index 38995ac32..56a231947 100644 --- a/apps/openmw/mwgui/levelupdialog.cpp +++ b/apps/openmw/mwgui/levelupdialog.cpp @@ -5,6 +5,7 @@ #include "../mwbase/windowmanager.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" +#include "../mwbase/soundmanager.hpp" #include "../mwworld/class.hpp" #include "../mwworld/fallback.hpp" @@ -16,15 +17,17 @@ namespace MWGui { - + const unsigned int LevelupDialog::sMaxCoins = 3; LevelupDialog::LevelupDialog() - : WindowBase("openmw_levelup_dialog.layout") + : WindowBase("openmw_levelup_dialog.layout"), + mCoinCount(sMaxCoins) { getWidget(mOkButton, "OkButton"); getWidget(mClassImage, "ClassImage"); getWidget(mLevelText, "LevelText"); getWidget(mLevelDescription, "LevelDescription"); getWidget(mCoinBox, "Coins"); + getWidget(mAssignWidget, "AssignWidget"); mOkButton->eventMouseButtonClick += MyGUI::newDelegate(this, &LevelupDialog::onOkButtonClicked); @@ -46,12 +49,10 @@ namespace MWGui mAttributeMultipliers.push_back(t); } - int curX = mMainWidget->getWidth()/2 - (16 + 2) * 1.5; - for (int i=0; i<3; ++i) + for (unsigned int i = 0; i < mCoinCount; ++i) { - MyGUI::ImageBox* image = mMainWidget->createWidget("ImageBox", MyGUI::IntCoord(curX,250,16,16), MyGUI::Align::Default); + MyGUI::ImageBox* image = mCoinBox->createWidget("ImageBox", MyGUI::IntCoord(0,0,16,16), MyGUI::Align::Default); image->setImageTexture ("icons\\tx_goldicon.dds"); - curX += 24+2; mCoins.push_back(image); } @@ -61,15 +62,15 @@ namespace MWGui void LevelupDialog::setAttributeValues() { MWWorld::Ptr player = MWBase::Environment::get().getWorld ()->getPlayerPtr(); - MWMechanics::CreatureStats& creatureStats = player.getClass().getCreatureStats (player); + MWMechanics::CreatureStats& creatureStats = player.getClass().getCreatureStats(player); MWMechanics::NpcStats& pcStats = player.getClass().getNpcStats (player); - for (int i=0; i<8; ++i) + for (int i = 0; i < 8; ++i) { - int val = creatureStats.getAttribute (i).getBase (); + int val = creatureStats.getAttribute(i).getBase(); if (std::find(mSpentAttributes.begin(), mSpentAttributes.end(), i) != mSpentAttributes.end()) { - val += pcStats.getLevelupAttributeMultiplier (i); + val += pcStats.getLevelupAttributeMultiplier(i); } if (val >= 100) @@ -80,33 +81,41 @@ namespace MWGui } - void LevelupDialog::resetCoins () + void LevelupDialog::resetCoins() { - int curX = 0; - for (int i=0; i<3; ++i) + const int coinSpacing = 10; + int curX = mCoinBox->getWidth()/2 - (coinSpacing*(mCoinCount - 1) + 16*mCoinCount)/2; + for (unsigned int i=0; idetachFromWidget(); image->attachToWidget(mCoinBox); - image->setCoord(MyGUI::IntCoord(curX,0,16,16)); - curX += 24+2; + if (i < mCoinCount) + { + mCoins[i]->setVisible(true); + image->setCoord(MyGUI::IntCoord(curX,0,16,16)); + curX += 16+coinSpacing; + } + else + mCoins[i]->setVisible(false); } } - void LevelupDialog::assignCoins () + void LevelupDialog::assignCoins() { resetCoins(); for (unsigned int i=0; idetachFromWidget(); - image->attachToWidget(mMainWidget); + image->attachToWidget(mAssignWidget); int attribute = mSpentAttributes[i]; - int xdiff = mAttributeMultipliers[attribute]->getCaption() == "" ? 0 : 30; + int xdiff = mAttributeMultipliers[attribute]->getCaption() == "" ? 0 : 20; - MyGUI::IntPoint pos = mAttributes[attribute]->getAbsolutePosition() - mMainWidget->getAbsolutePosition() - MyGUI::IntPoint(24+xdiff,-4); + MyGUI::IntPoint pos = mAttributes[attribute]->getAbsolutePosition() - mAssignWidget->getAbsolutePosition() - MyGUI::IntPoint(22+xdiff,0); + pos.top += (mAttributes[attribute]->getHeight() - image->getHeight())/2; image->setPosition(pos); } @@ -117,13 +126,8 @@ namespace MWGui { MWBase::World *world = MWBase::Environment::get().getWorld(); MWWorld::Ptr player = world->getPlayerPtr(); - MWMechanics::CreatureStats& creatureStats = player.getClass().getCreatureStats (player); - MWMechanics::NpcStats& pcStats = player.getClass().getNpcStats (player); - - mSpentAttributes.clear(); - resetCoins(); - - setAttributeValues(); + MWMechanics::CreatureStats& creatureStats = player.getClass().getCreatureStats(player); + MWMechanics::NpcStats& pcStats = player.getClass().getNpcStats(player); const ESM::NPC *playerData = player.get()->mBase; @@ -131,82 +135,106 @@ namespace MWGui const ESM::Class *cls = world->getStore().get().find(playerData->mClass); - // Vanilla uses thief.dds for custom classes. A player with a custom class - // doesn't have mId set, see mwworld/esmstore.hpp where it is initialised as - // "$dynamic0". This check should resolve bug #1260. - // Choosing Stealth specialization and Speed/Agility as attributes. if(world->getStore().get().isDynamic(cls->mId)) { + // Vanilla uses thief.dds for custom classes. + // Choosing Stealth specialization and Speed/Agility as attributes, if possible. Otherwise fall back to first class found. MWWorld::SharedIterator it = world->getStore().get().begin(); - for(; it != world->getStore().get().end(); it++) + for(; it != world->getStore().get().end(); ++it) { if(it->mData.mIsPlayable && it->mData.mSpecialization == 2 && it->mData.mAttribute[0] == 4 && it->mData.mAttribute[1] == 3) break; } - mClassImage->setImageTexture ("textures\\levelup\\" + it->mId + ".dds"); + if (it == world->getStore().get().end()) + it = world->getStore().get().begin(); + if (it != world->getStore().get().end()) + cls = &*it; } - else - mClassImage->setImageTexture ("textures\\levelup\\" + cls->mId + ".dds"); + + mClassImage->setImageTexture ("textures\\levelup\\" + cls->mId + ".dds"); int level = creatureStats.getLevel ()+1; mLevelText->setCaptionWithReplacing("#{sLevelUpMenu1} " + boost::lexical_cast(level)); std::string levelupdescription; - if(level>20) + if(level > 20) levelupdescription=world->getFallback()->getFallbackString("Level_Up_Default"); else levelupdescription=world->getFallback()->getFallbackString("Level_Up_Level"+boost::lexical_cast(level)); mLevelDescription->setCaption (levelupdescription); - for (int i=0; i<8; ++i) + unsigned int availableAttributes = 0; + for (int i = 0; i < 8; ++i) { MyGUI::TextBox* text = mAttributeMultipliers[i]; - int mult = pcStats.getLevelupAttributeMultiplier (i); - text->setCaption(mult <= 1 ? "" : "x" + boost::lexical_cast(mult)); + if (pcStats.getAttribute(i).getBase() < 100) + { + mAttributes[i]->setEnabled(true); + availableAttributes++; + + int mult = pcStats.getLevelupAttributeMultiplier (i); + text->setCaption(mult <= 1 ? "" : "x" + boost::lexical_cast(mult)); + } + else + { + mAttributes[i]->setEnabled(false); + + text->setCaption(""); + } } + mCoinCount = std::min(sMaxCoins, availableAttributes); + + mSpentAttributes.clear(); + resetCoins(); + + setAttributeValues(); + center(); + + // Play LevelUp Music + MWBase::Environment::get().getSoundManager()->streamMusic("Special/MW_Triumph.mp3"); } - void LevelupDialog::onOkButtonClicked (MyGUI::Widget* sender) + void LevelupDialog::onOkButtonClicked(MyGUI::Widget* sender) { - MWWorld::Ptr player = MWBase::Environment::get().getWorld ()->getPlayerPtr(); + MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); MWMechanics::NpcStats& pcStats = player.getClass().getNpcStats (player); - if (mSpentAttributes.size() < 3) - MWBase::Environment::get().getWindowManager ()->messageBox("#{sNotifyMessage36}"); + if (mSpentAttributes.size() < mCoinCount) + MWBase::Environment::get().getWindowManager()->messageBox("#{sNotifyMessage36}"); else { // increase attributes - for (int i=0; i<3; ++i) + for (unsigned int i = 0; i < mCoinCount; ++i) { MWMechanics::AttributeValue attribute = pcStats.getAttribute(mSpentAttributes[i]); - attribute.setBase (attribute.getBase () + pcStats.getLevelupAttributeMultiplier (mSpentAttributes[i])); + attribute.setBase(attribute.getBase() + pcStats.getLevelupAttributeMultiplier(mSpentAttributes[i])); if (attribute.getBase() >= 100) attribute.setBase(100); pcStats.setAttribute(mSpentAttributes[i], attribute); } - pcStats.levelUp (); + pcStats.levelUp(); - MWBase::Environment::get().getWindowManager()->removeGuiMode (GM_Levelup); + MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Levelup); } } - void LevelupDialog::onAttributeClicked (MyGUI::Widget *sender) + void LevelupDialog::onAttributeClicked(MyGUI::Widget *sender) { int attribute = *sender->getUserData(); std::vector::iterator found = std::find(mSpentAttributes.begin(), mSpentAttributes.end(), attribute); if (found != mSpentAttributes.end()) - mSpentAttributes.erase (found); + mSpentAttributes.erase(found); else { - if (mSpentAttributes.size() == 3) - mSpentAttributes[2] = attribute; + if (mSpentAttributes.size() == mCoinCount) + mSpentAttributes[mCoinCount - 1] = attribute; else mSpentAttributes.push_back(attribute); } diff --git a/apps/openmw/mwgui/levelupdialog.hpp b/apps/openmw/mwgui/levelupdialog.hpp index 69afbf089..e5edbd53c 100644 --- a/apps/openmw/mwgui/levelupdialog.hpp +++ b/apps/openmw/mwgui/levelupdialog.hpp @@ -20,6 +20,7 @@ namespace MWGui MyGUI::EditBox* mLevelDescription; MyGUI::Widget* mCoinBox; + MyGUI::Widget* mAssignWidget; std::vector mAttributes; std::vector mAttributeValues; @@ -28,8 +29,11 @@ namespace MWGui std::vector mSpentAttributes; - void onOkButtonClicked (MyGUI::Widget* sender); - void onAttributeClicked (MyGUI::Widget* sender); + unsigned int mCoinCount; + static const unsigned int sMaxCoins; + + void onOkButtonClicked(MyGUI::Widget* sender); + void onAttributeClicked(MyGUI::Widget* sender); void assignCoins(); void resetCoins(); diff --git a/apps/openmw/mwgui/list.cpp b/apps/openmw/mwgui/list.cpp deleted file mode 100644 index 19f20eeee..000000000 --- a/apps/openmw/mwgui/list.cpp +++ /dev/null @@ -1,155 +0,0 @@ -#include "list.hpp" - -#include -#include -#include -#include - -namespace MWGui -{ - - namespace Widgets - { - - MWList::MWList() : - mClient(0) - , mScrollView(0) - , mItemHeight(0) - { - } - - void MWList::initialiseOverride() - { - Base::initialiseOverride(); - - assignWidget(mClient, "Client"); - if (mClient == 0) - mClient = this; - - mScrollView = mClient->createWidgetReal( - "MW_ScrollView", MyGUI::FloatCoord(0.0, 0.0, 1.0, 1.0), - MyGUI::Align::Top | MyGUI::Align::Left | MyGUI::Align::Stretch, getName() + "_ScrollView"); - } - - void MWList::addItem(const std::string& name) - { - mItems.push_back(name); - } - - void MWList::addSeparator() - { - mItems.push_back(""); - } - - void MWList::adjustSize() - { - redraw(); - } - - void MWList::redraw(bool scrollbarShown) - { - const int _scrollBarWidth = 20; // fetch this from skin? - const int scrollBarWidth = scrollbarShown ? _scrollBarWidth : 0; - const int spacing = 3; - size_t viewPosition = -mScrollView->getViewOffset().top; - - while (mScrollView->getChildCount()) - { - MyGUI::Gui::getInstance().destroyWidget(mScrollView->getChildAt(0)); - } - - mItemHeight = 0; - int i=0; - for (std::vector::const_iterator it=mItems.begin(); - it!=mItems.end(); ++it) - { - if (*it != "") - { - MyGUI::Button* button = mScrollView->createWidget( - "MW_ListLine", MyGUI::IntCoord(0, mItemHeight, mScrollView->getSize().width - scrollBarWidth - 2, 24), - MyGUI::Align::Left | MyGUI::Align::Top, getName() + "_item_" + (*it)); - button->setCaption((*it)); - button->getSubWidgetText()->setWordWrap(true); - button->getSubWidgetText()->setTextAlign(MyGUI::Align::Left); - button->eventMouseWheel += MyGUI::newDelegate(this, &MWList::onMouseWheel); - button->eventMouseButtonClick += MyGUI::newDelegate(this, &MWList::onItemSelected); - - int height = button->getTextSize().height; - button->setSize(MyGUI::IntSize(button->getSize().width, height)); - button->setUserData(i); - - mItemHeight += height + spacing; - } - else - { - MyGUI::ImageBox* separator = mScrollView->createWidget("MW_HLine", - MyGUI::IntCoord(2, mItemHeight, mScrollView->getWidth() - scrollBarWidth - 4, 18), - MyGUI::Align::Left | MyGUI::Align::Top | MyGUI::Align::HStretch); - separator->setNeedMouseFocus(false); - - mItemHeight += 18 + spacing; - } - ++i; - } - mScrollView->setCanvasSize(mClient->getSize().width, std::max(mItemHeight, mClient->getSize().height)); - - if (!scrollbarShown && mItemHeight > mClient->getSize().height) - redraw(true); - - size_t viewRange = mScrollView->getCanvasSize().height; - if(viewPosition > viewRange) - viewPosition = viewRange; - mScrollView->setViewOffset(MyGUI::IntPoint(0, -viewPosition)); - } - - bool MWList::hasItem(const std::string& name) - { - return (std::find(mItems.begin(), mItems.end(), name) != mItems.end()); - } - - unsigned int MWList::getItemCount() - { - return mItems.size(); - } - - std::string MWList::getItemNameAt(unsigned int at) - { - assert(at < mItems.size() && "List item out of bounds"); - return mItems[at]; - } - - void MWList::removeItem(const std::string& name) - { - assert( std::find(mItems.begin(), mItems.end(), name) != mItems.end() ); - mItems.erase( std::find(mItems.begin(), mItems.end(), name) ); - } - - void MWList::clear() - { - mItems.clear(); - } - - void MWList::onMouseWheel(MyGUI::Widget* _sender, int _rel) - { - //NB view offset is negative - if (mScrollView->getViewOffset().top + _rel*0.3 > 0) - mScrollView->setViewOffset(MyGUI::IntPoint(0, 0)); - else - mScrollView->setViewOffset(MyGUI::IntPoint(0, mScrollView->getViewOffset().top + _rel*0.3)); - } - - void MWList::onItemSelected(MyGUI::Widget* _sender) - { - std::string name = static_cast(_sender)->getCaption(); - int id = *_sender->getUserData(); - eventItemSelected(name, id); - eventWidgetSelected(_sender); - } - - MyGUI::Widget* MWList::getItemWidget(const std::string& name) - { - return mScrollView->findWidget (getName() + "_item_" + name); - } - - } -} diff --git a/apps/openmw/mwgui/list.hpp b/apps/openmw/mwgui/list.hpp deleted file mode 100644 index dcfe7931a..000000000 --- a/apps/openmw/mwgui/list.hpp +++ /dev/null @@ -1,71 +0,0 @@ -#ifndef MWGUI_LIST_HPP -#define MWGUI_LIST_HPP - -#include - -namespace MWGui -{ - namespace Widgets - { - /** - * \brief a very simple list widget that supports word-wrapping entries - * \note if the width or height of the list changes, you must call adjustSize() method - */ - class MWList : public MyGUI::Widget - { - MYGUI_RTTI_DERIVED(MWList) - public: - MWList(); - - typedef MyGUI::delegates::CMultiDelegate2 EventHandle_StringInt; - typedef MyGUI::delegates::CMultiDelegate1 EventHandle_Widget; - - /** - * Event: Item selected with the mouse. - * signature: void method(std::string itemName) - */ - EventHandle_StringInt eventItemSelected; - - /** - * Event: Item selected with the mouse. - * signature: void method(MyGUI::Widget* sender) - */ - EventHandle_Widget eventWidgetSelected; - - - /** - * Call after the size of the list changed, or items were inserted/removed - */ - void adjustSize(); - - void addItem(const std::string& name); - void addSeparator(); ///< add a seperator between the current and the next item. - void removeItem(const std::string& name); - bool hasItem(const std::string& name); - unsigned int getItemCount(); - std::string getItemNameAt(unsigned int at); ///< \attention if there are separators, this method will return "" at the place where the separator is - void clear(); - - MyGUI::Widget* getItemWidget(const std::string& name); - ///< get widget for an item name, useful to set up tooltip - - protected: - void initialiseOverride(); - - void redraw(bool scrollbarShown = false); - - void onMouseWheel(MyGUI::Widget* _sender, int _rel); - void onItemSelected(MyGUI::Widget* _sender); - - private: - MyGUI::ScrollView* mScrollView; - MyGUI::Widget* mClient; - - std::vector mItems; - - int mItemHeight; // height of all items - }; - } -} - -#endif diff --git a/apps/openmw/mwgui/loadingscreen.cpp b/apps/openmw/mwgui/loadingscreen.cpp index 7c915ebeb..a446266d9 100644 --- a/apps/openmw/mwgui/loadingscreen.cpp +++ b/apps/openmw/mwgui/loadingscreen.cpp @@ -11,6 +11,7 @@ #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" +#include "../mwbase/statemanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/inputmanager.hpp" @@ -26,10 +27,11 @@ namespace MWGui , WindowBase("openmw_loading_screen.layout") , mLastRenderTime(0.f) , mLastWallpaperChangeTime(0.f) - , mFirstLoad(true) , mProgress(0) , mVSyncWasEnabled(false) { + mMainWidget->setSize(MyGUI::RenderManager::getInstance().getViewSize()); + getWidget(mLoadingText, "LoadingText"); getWidget(mProgressBar, "ProgressBar"); @@ -56,13 +58,6 @@ namespace MWGui mBackgroundImage->setVisible(visible); } - void LoadingScreen::onResChange(int w, int h) - { - setCoord(0,0,w,h); - - mBackgroundImage->setCoord(MyGUI::IntCoord(0,0,w,h)); - } - void LoadingScreen::loadingOn() { // Early-out if already on @@ -71,13 +66,14 @@ namespace MWGui // Temporarily turn off VSync, we want to do actual loading rather than waiting for the screen to sync. // Threaded loading would be even better, of course - especially because some drivers force VSync to on and we can't change it. - // In Ogre 1.8, the swapBuffers argument is useless and setVSyncEnabled is bugged with GLX, nothing we can do :/ mVSyncWasEnabled = mWindow->isVSyncEnabled(); - #if OGRE_VERSION >= (1 << 16 | 9 << 8 | 0) mWindow->setVSyncEnabled(false); - #endif - if (!mFirstLoad) + bool showWallpaper = (MWBase::Environment::get().getStateManager()->getState() + == MWBase::StateManager::State_NoGame); + + + if (!showWallpaper) { mBackgroundImage->setImageTexture(""); int width = mWindow->getWidth(); @@ -103,21 +99,18 @@ namespace MWGui setVisible(true); - if (mFirstLoad) + if (showWallpaper) { changeWallpaper(); } - MWBase::Environment::get().getWindowManager()->pushGuiMode(mFirstLoad ? GM_LoadingWallpaper : GM_Loading); + MWBase::Environment::get().getWindowManager()->pushGuiMode(showWallpaper ? GM_LoadingWallpaper : GM_Loading); } void LoadingScreen::loadingOff() { // Re-enable vsync now. - // In Ogre 1.8, the swapBuffers argument is useless and setVSyncEnabled is bugged with GLX, nothing we can do :/ - #if OGRE_VERSION >= (1 << 16 | 9 << 8 | 0) mWindow->setVSyncEnabled(mVSyncWasEnabled); - #endif setVisible(false); @@ -132,7 +125,7 @@ namespace MWGui Ogre::StringVector groups = Ogre::ResourceGroupManager::getSingleton().getResourceGroups (); for (Ogre::StringVector::iterator it = groups.begin(); it != groups.end(); ++it) { - Ogre::StringVectorPtr resourcesInThisGroup = Ogre::ResourceGroupManager::getSingleton ().findResourceNames (*it, "Splash_*.tga"); + Ogre::StringVectorPtr resourcesInThisGroup = Ogre::ResourceGroupManager::getSingleton ().findResourceNames (*it, "Splash/*.tga"); mResources.insert(mResources.end(), resourcesInThisGroup->begin(), resourcesInThisGroup->end()); } } @@ -188,11 +181,6 @@ namespace MWGui draw(); } - void LoadingScreen::removeWallpaper() - { - mFirstLoad = false; - } - void LoadingScreen::draw() { const float loadingScreenFps = 20.f; @@ -201,7 +189,10 @@ namespace MWGui { mLastRenderTime = mTimer.getMilliseconds (); - if (mFirstLoad && mTimer.getMilliseconds () > mLastWallpaperChangeTime + 5000*1) + bool showWallpaper = (MWBase::Environment::get().getStateManager()->getState() + == MWBase::StateManager::State_NoGame); + + if (showWallpaper && mTimer.getMilliseconds () > mLastWallpaperChangeTime + 5000*1) { mLastWallpaperChangeTime = mTimer.getMilliseconds (); changeWallpaper(); diff --git a/apps/openmw/mwgui/loadingscreen.hpp b/apps/openmw/mwgui/loadingscreen.hpp index 96e0e1ed4..310a6df3c 100644 --- a/apps/openmw/mwgui/loadingscreen.hpp +++ b/apps/openmw/mwgui/loadingscreen.hpp @@ -29,21 +29,15 @@ namespace MWGui virtual void setVisible(bool visible); - virtual void removeWallpaper(); - LoadingScreen(Ogre::SceneManager* sceneMgr, Ogre::RenderWindow* rw); virtual ~LoadingScreen(); void setLoadingProgress (const std::string& stage, int depth, int current, int total); void loadingDone(); - void onResChange(int w, int h); - void updateWindow(Ogre::RenderWindow* rw) { mWindow = rw; } private: - bool mFirstLoad; - Ogre::SceneManager* mSceneMgr; Ogre::RenderWindow* mWindow; diff --git a/apps/openmw/mwgui/mainmenu.cpp b/apps/openmw/mwgui/mainmenu.cpp index bafd04311..bb003c481 100644 --- a/apps/openmw/mwgui/mainmenu.cpp +++ b/apps/openmw/mwgui/mainmenu.cpp @@ -1,7 +1,11 @@ #include "mainmenu.hpp" +#include + #include +#include + #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/soundmanager.hpp" @@ -14,8 +18,8 @@ #include "savegamedialog.hpp" #include "confirmationdialog.hpp" -#include "imagebutton.hpp" #include "backgroundimage.hpp" +#include "videowidget.hpp" namespace MWGui { @@ -25,6 +29,8 @@ namespace MWGui , mButtonBox(0), mWidth (w), mHeight (h) , mSaveGameDialog(NULL) , mBackground(NULL) + , mVideoBackground(NULL) + , mVideo(NULL) { getWidget(mVersionText, "VersionText"); std::stringstream sstream; @@ -38,10 +44,12 @@ namespace MWGui rev = rev.substr(0,10); sstream << "\nRevision: " << rev; } - + std::string output = sstream.str(); mVersionText->setCaption(output); + mHasAnimatedMenu = (Ogre::ResourceGroupManager::getSingleton().resourceExistsInAnyGroup("video\\menu_background.bik")); + updateMenu(); } @@ -62,10 +70,10 @@ namespace MWGui { if (visible) updateMenu(); - else - showBackground( - MWBase::Environment::get().getWindowManager()->containsMode(MWGui::GM_MainMenu) && - MWBase::Environment::get().getStateManager()->getState() == MWBase::StateManager::State_NoGame); + + showBackground( + MWBase::Environment::get().getWindowManager()->containsMode(MWGui::GM_MainMenu) && + MWBase::Environment::get().getStateManager()->getState() == MWBase::StateManager::State_NoGame); OEngine::GUI::Layout::setVisible (visible); } @@ -86,7 +94,6 @@ namespace MWGui MWBase::Environment::get().getSoundManager()->playSound("Menu Click", 1.f, 1.f); if (name == "return") { - MWBase::Environment::get().getSoundManager ()->resumeSounds (MWBase::SoundManager::Play_TypeSfx); MWBase::Environment::get().getWindowManager ()->removeGuiMode (GM_MainMenu); } else if (name == "options") @@ -134,14 +141,73 @@ namespace MWGui void MainMenu::showBackground(bool show) { - if (show && !mBackground) + if (mVideo && !show) + { + MyGUI::Gui::getInstance().destroyWidget(mVideoBackground); + mVideoBackground = NULL; + mVideo = NULL; + } + if (mBackground && !show) { - mBackground = MyGUI::Gui::getInstance().createWidgetReal("ImageBox", 0,0,1,1, - MyGUI::Align::Stretch, "Menu"); - mBackground->setBackgroundImage("textures\\menu_morrowind.dds"); + MyGUI::Gui::getInstance().destroyWidget(mBackground); + mBackground = NULL; + } + + if (!show) + return; + + if (mHasAnimatedMenu) + { + if (!mVideo) + { + // Use black background to correct aspect ratio + mVideoBackground = MyGUI::Gui::getInstance().createWidgetReal("ImageBox", 0,0,1,1, + MyGUI::Align::Default, "Menu"); + mVideoBackground->setImageTexture("black.png"); + + mVideo = mVideoBackground->createWidget("ImageBox", 0,0,1,1, + MyGUI::Align::Stretch, "Menu"); + + mVideo->playVideo("video\\menu_background.bik"); + } + + MyGUI::IntSize viewSize = MyGUI::RenderManager::getInstance().getViewSize(); + int screenWidth = viewSize.width; + int screenHeight = viewSize.height; + mVideoBackground->setSize(screenWidth, screenHeight); + + double imageaspect = static_cast(mVideo->getVideoWidth())/mVideo->getVideoHeight(); + + int leftPadding = std::max(0.0, (screenWidth - screenHeight * imageaspect) / 2); + int topPadding = std::max(0.0, (screenHeight - screenWidth / imageaspect) / 2); + + mVideo->setCoord(leftPadding, topPadding, + screenWidth - leftPadding*2, screenHeight - topPadding*2); + + mVideo->setVisible(true); + } + else + { + if (!mBackground) + { + mBackground = MyGUI::Gui::getInstance().createWidgetReal("ImageBox", 0,0,1,1, + MyGUI::Align::Stretch, "Menu"); + mBackground->setBackgroundImage("textures\\menu_morrowind.dds"); + } + mBackground->setVisible(true); + } + } + + void MainMenu::update(float dt) + { + if (mVideo) + { + if (!mVideo->update()) + { + // If finished playing, start again + mVideo->playVideo("video\\menu_background.bik"); + } } - if (mBackground) - mBackground->setVisible(show); } void MainMenu::updateMenu() @@ -155,7 +221,6 @@ namespace MWGui MWBase::StateManager::State state = MWBase::Environment::get().getStateManager()->getState(); - showBackground(state == MWBase::StateManager::State_NoGame); mVersionText->setVisible(state == MWBase::StateManager::State_NoGame); std::vector buttons; @@ -186,7 +251,7 @@ namespace MWGui { if (mButtons.find(*it) == mButtons.end()) { - MWGui::ImageButton* button = mButtonBox->createWidget + Gui::ImageButton* button = mButtonBox->createWidget ("ImageBox", MyGUI::IntCoord(0, curH, 0, 0), MyGUI::Align::Default); button->setProperty("ImageHighlighted", "textures\\menu_" + *it + "_over.dds"); button->setProperty("ImageNormal", "textures\\menu_" + *it + ".dds"); @@ -199,7 +264,7 @@ namespace MWGui // Start by hiding all buttons int maxwidth = 0; - for (std::map::iterator it = mButtons.begin(); it != mButtons.end(); ++it) + for (std::map::iterator it = mButtons.begin(); it != mButtons.end(); ++it) { it->second->setVisible(false); MyGUI::IntSize requested = it->second->getRequestedSize(); @@ -211,7 +276,7 @@ namespace MWGui for (std::vector::iterator it = buttons.begin(); it != buttons.end(); ++it) { assert(mButtons.find(*it) != mButtons.end()); - MWGui::ImageButton* button = mButtons[*it]; + Gui::ImageButton* button = mButtons[*it]; button->setVisible(true); MyGUI::IntSize requested = button->getRequestedSize(); @@ -229,7 +294,7 @@ namespace MWGui if (state == MWBase::StateManager::State_NoGame) { // Align with the background image - int bottomPadding=48; + int bottomPadding=24; mButtonBox->setCoord (mWidth/2 - maxwidth/2, mHeight - curH - bottomPadding, maxwidth, curH); } else diff --git a/apps/openmw/mwgui/mainmenu.hpp b/apps/openmw/mwgui/mainmenu.hpp index c27442536..cd2050d0f 100644 --- a/apps/openmw/mwgui/mainmenu.hpp +++ b/apps/openmw/mwgui/mainmenu.hpp @@ -3,18 +3,25 @@ #include +namespace Gui +{ + class ImageButton; +} + namespace MWGui { - class ImageButton; class BackgroundImage; class SaveGameDialog; + class VideoWidget; class MainMenu : public OEngine::GUI::Layout { int mWidth; int mHeight; + bool mHasAnimatedMenu; + public: MainMenu(int w, int h); @@ -24,6 +31,8 @@ namespace MWGui virtual void setVisible (bool visible); + void update(float dt); + private: MyGUI::Widget* mButtonBox; @@ -31,7 +40,10 @@ namespace MWGui BackgroundImage* mBackground; - std::map mButtons; + MyGUI::ImageBox* mVideoBackground; + VideoWidget* mVideo; // For animated main menus + + std::map mButtons; void onButtonClicked (MyGUI::Widget* sender); void onNewGameConfirmed(); diff --git a/apps/openmw/mwgui/mapwindow.cpp b/apps/openmw/mwgui/mapwindow.cpp index 29c065f3d..0262c4d0e 100644 --- a/apps/openmw/mwgui/mapwindow.cpp +++ b/apps/openmw/mwgui/mapwindow.cpp @@ -17,48 +17,171 @@ #include "../components/esm/globalmap.hpp" #include "widgets.hpp" +#include "confirmationdialog.hpp" + +namespace +{ + + const int widgetSize = 512; + + const int cellSize = 8192; + + enum LocalMapWidgetDepth + { + Local_CompassLayer = 0, + Local_MarkerAboveFogLayer = 1, + Local_FogLayer = 2, + Local_MarkerLayer = 3, + Local_MapLayer = 4 + }; + + enum GlobalMapWidgetDepth + { + Global_CompassLayer = 0, + Global_MarkerLayer = 1, + Global_ExploreOverlayLayer = 2, + Global_MapLayer = 3 + }; + + + /// @brief A widget that changes its color when hovered. + class MarkerWidget: public MyGUI::Widget + { + MYGUI_RTTI_DERIVED(MarkerWidget) + + public: + void setNormalColour(const MyGUI::Colour& colour) + { + mNormalColour = colour; + setColour(colour); + } + + void setHoverColour(const MyGUI::Colour& colour) + { + mHoverColour = colour; + } + + private: + MyGUI::Colour mNormalColour; + MyGUI::Colour mHoverColour; + + void onMouseLostFocus(MyGUI::Widget* _new) + { + setColour(mNormalColour); + } + + void onMouseSetFocus(MyGUI::Widget* _old) + { + setColour(mHoverColour); + } + }; +} namespace MWGui { - LocalMapBase::LocalMapBase() + void CustomMarker::save(ESM::ESMWriter &esm) const + { + esm.writeHNT("POSX", mWorldX); + esm.writeHNT("POSY", mWorldY); + mCell.save(esm); + if (!mNote.empty()) + esm.writeHNString("NOTE", mNote); + } + + void CustomMarker::load(ESM::ESMReader &esm) + { + esm.getHNT(mWorldX, "POSX"); + esm.getHNT(mWorldY, "POSY"); + mCell.load(esm); + mNote = esm.getHNOString("NOTE"); + } + + // ------------------------------------------------------ + + void CustomMarkerCollection::addMarker(const CustomMarker &marker, bool triggerEvent) + { + mMarkers.push_back(marker); + if (triggerEvent) + eventMarkersChanged(); + } + + void CustomMarkerCollection::deleteMarker(const CustomMarker &marker) + { + std::vector::iterator it = std::find(mMarkers.begin(), mMarkers.end(), marker); + if (it != mMarkers.end()) + mMarkers.erase(it); + else + throw std::runtime_error("can't find marker to delete"); + + eventMarkersChanged(); + } + + void CustomMarkerCollection::updateMarker(const CustomMarker &marker, const std::string &newNote) + { + std::vector::iterator it = std::find(mMarkers.begin(), mMarkers.end(), marker); + if (it != mMarkers.end()) + it->mNote = newNote; + else + throw std::runtime_error("can't find marker to update"); + + eventMarkersChanged(); + } + + void CustomMarkerCollection::clear() + { + mMarkers.clear(); + eventMarkersChanged(); + } + + std::vector::const_iterator CustomMarkerCollection::begin() const + { + return mMarkers.begin(); + } + + std::vector::const_iterator CustomMarkerCollection::end() const + { + return mMarkers.end(); + } + + size_t CustomMarkerCollection::size() const + { + return mMarkers.size(); + } + + // ------------------------------------------------------ + + LocalMapBase::LocalMapBase(CustomMarkerCollection &markers) : mCurX(0) , mCurY(0) , mInterior(false) , mFogOfWar(true) , mLocalMap(NULL) - , mMapDragAndDrop(false) , mPrefix() , mChanged(true) - , mLayout(NULL) - , mLastPositionX(0.0f) - , mLastPositionY(0.0f) , mLastDirectionX(0.0f) , mLastDirectionY(0.0f) , mCompass(NULL) + , mMarkerUpdateTimer(0.0f) + , mCustomMarkers(markers) { + mCustomMarkers.eventMarkersChanged += MyGUI::newDelegate(this, &LocalMapBase::updateCustomMarkers); } LocalMapBase::~LocalMapBase() { - // Clear our "lost focus" delegate for marker widgets first, otherwise it will - // fire when the widget is about to be destroyed and the mouse cursor is over it. - // At that point, other widgets may already be destroyed, so applyFogOfWar (which is called by the delegate) would crash. - for (std::vector::iterator it = mDoorMarkerWidgets.begin(); it != mDoorMarkerWidgets.end(); ++it) - (*it)->eventMouseLostFocus.clear(); - for (std::vector::iterator it = mMarkerWidgets.begin(); it != mMarkerWidgets.end(); ++it) - (*it)->eventMouseLostFocus.clear(); + mCustomMarkers.eventMarkersChanged -= MyGUI::newDelegate(this, &LocalMapBase::updateCustomMarkers); } - void LocalMapBase::init(MyGUI::ScrollView* widget, MyGUI::ImageBox* compass, OEngine::GUI::Layout* layout, bool mapDragAndDrop) + void LocalMapBase::init(MyGUI::ScrollView* widget, MyGUI::ImageBox* compass) { mLocalMap = widget; - mLayout = layout; - mMapDragAndDrop = mapDragAndDrop; mCompass = compass; + mCompass->setDepth(Local_CompassLayer); + mCompass->setNeedMouseFocus(false); + // create 3x3 map widgets, 512x512 each, holding a 1024x1024 texture each - const int widgetSize = 512; for (int mx=0; mx<3; ++mx) { for (int my=0; my<3; ++my) @@ -66,16 +189,15 @@ namespace MWGui MyGUI::ImageBox* map = mLocalMap->createWidget("ImageBox", MyGUI::IntCoord(mx*widgetSize, my*widgetSize, widgetSize, widgetSize), MyGUI::Align::Top | MyGUI::Align::Left); + map->setDepth(Local_MapLayer); - MyGUI::ImageBox* fog = map->createWidget("ImageBox", - MyGUI::IntCoord(0, 0, widgetSize, widgetSize), + MyGUI::ImageBox* fog = mLocalMap->createWidget("ImageBox", + MyGUI::IntCoord(mx*widgetSize, my*widgetSize, widgetSize, widgetSize), MyGUI::Align::Top | MyGUI::Align::Left); + fog->setDepth(Local_FogLayer); - if (!mMapDragAndDrop) - { - map->setNeedMouseFocus(false); - fog->setNeedMouseFocus(false); - } + map->setNeedMouseFocus(false); + fog->setNeedMouseFocus(false); mMapWidgets.push_back(map); mFogWidgets.push_back(fog); @@ -111,19 +233,7 @@ namespace MWGui : ""); } } - notifyMapChanged (); - } - - void LocalMapBase::onMarkerFocused (MyGUI::Widget* w1, MyGUI::Widget* w2) - { - // Workaround to not make the marker visible if it's under fog of war - applyFogOfWar (); - } - - void LocalMapBase::onMarkerUnfocused (MyGUI::Widget* w1, MyGUI::Widget* w2) - { - // Workaround to not make the marker visible if it's under fog of war - applyFogOfWar (); + redraw(); } MyGUI::IntPoint LocalMapBase::getMarkerPosition(float worldX, float worldY, MarkerPosition& markerPos) @@ -138,7 +248,6 @@ namespace MWGui { int cellX, cellY; MWBase::Environment::get().getWorld()->positionToIndex(worldX, worldY, cellX, cellY); - const int cellSize = 8192; nX = (worldX - cellSize * cellX) / cellSize; // Image space is -Y up, cells are Y up nY = 1 - (worldY - cellSize * cellY) / cellSize; @@ -149,21 +258,21 @@ namespace MWGui markerPos.cellX = cellX; markerPos.cellY = cellY; - widgetPos = MyGUI::IntPoint(nX * 512 + (1+cellDx) * 512, - nY * 512 - (cellDy-1) * 512); + widgetPos = MyGUI::IntPoint(nX * widgetSize + (1+cellDx) * widgetSize, + nY * widgetSize - (cellDy-1) * widgetSize); } else { int cellX, cellY; Ogre::Vector2 worldPos (worldX, worldY); - MWBase::Environment::get().getWorld ()->getInteriorMapPosition (worldPos, nX, nY, cellX, cellY); + MWBase::Environment::get().getWorld ()->worldToInteriorMapPosition (worldPos, nX, nY, cellX, cellY); markerPos.cellX = cellX; markerPos.cellY = cellY; // Image space is -Y up, cells are Y up - widgetPos = MyGUI::IntPoint(nX * 512 + (1+(cellX-mCurX)) * 512, - nY * 512 + (1-(cellY-mCurY)) * 512); + widgetPos = MyGUI::IntPoint(nX * widgetSize + (1+(cellX-mCurX)) * widgetSize, + nY * widgetSize + (1-(cellY-mCurY)) * widgetSize); } markerPos.nX = nX; @@ -171,6 +280,53 @@ namespace MWGui return widgetPos; } + void LocalMapBase::updateCustomMarkers() + { + for (std::vector::iterator it = mCustomMarkerWidgets.begin(); it != mCustomMarkerWidgets.end(); ++it) + MyGUI::Gui::getInstance().destroyWidget(*it); + mCustomMarkerWidgets.clear(); + + for (std::vector::const_iterator it = mCustomMarkers.begin(); it != mCustomMarkers.end(); ++it) + { + const CustomMarker& marker = *it; + + if (marker.mCell.mPaged != !mInterior) + continue; + if (mInterior) + { + if (marker.mCell.mWorldspace != mPrefix) + continue; + } + else + { + if (std::abs(marker.mCell.mIndex.mX - mCurX) > 1) + continue; + if (std::abs(marker.mCell.mIndex.mY - mCurY) > 1) + continue; + } + + MarkerPosition markerPos; + MyGUI::IntPoint widgetPos = getMarkerPosition(marker.mWorldX, marker.mWorldY, markerPos); + + MyGUI::IntCoord widgetCoord(widgetPos.left - 4, + widgetPos.top - 4, + 8, 8); + MarkerWidget* markerWidget = mLocalMap->createWidget("MarkerButton", + widgetCoord, MyGUI::Align::Default); + markerWidget->setDepth(Local_MarkerAboveFogLayer); + markerWidget->setUserString("ToolTipType", "Layout"); + markerWidget->setUserString("ToolTipLayout", "TextToolTipOneLine"); + markerWidget->setUserString("Caption_TextOneLine", MyGUI::TextIterator::toTagsString(marker.mNote)); + markerWidget->setNormalColour(MyGUI::Colour(1.0,0.3,0.3)); + markerWidget->setHoverColour(MyGUI::Colour(1.0,0.5,0.5)); + markerWidget->setUserData(marker); + markerWidget->setNeedMouseFocus(true); + customMarkerCreated(markerWidget); + mCustomMarkerWidgets.push_back(markerWidget); + } + redraw(); + } + void LocalMapBase::setActiveCell(const int x, const int y, bool interior) { if (x==mCurX && y==mCurY && mInterior==interior && !mChanged) @@ -181,6 +337,9 @@ namespace MWGui mInterior = interior; mChanged = false; + applyFogOfWar(); + + // clear all previous door markers for (std::vector::iterator it = mDoorMarkerWidgets.begin(); it != mDoorMarkerWidgets.end(); ++it) MyGUI::Gui::getInstance().destroyWidget(*it); @@ -237,51 +396,49 @@ namespace MWGui widgetPos.top - 4, 8, 8); ++counter; - MyGUI::Button* markerWidget = mLocalMap->createWidget("ButtonImage", + MarkerWidget* markerWidget = mLocalMap->createWidget("MarkerButton", widgetCoord, MyGUI::Align::Default); - markerWidget->setImageResource("DoorMarker"); + markerWidget->setNormalColour(MyGUI::Colour::parse(MyGUI::LanguageManager::getInstance().replaceTags("#{fontcolour=normal}"))); + markerWidget->setHoverColour(MyGUI::Colour::parse(MyGUI::LanguageManager::getInstance().replaceTags("#{fontcolour=normal_over}"))); + markerWidget->setDepth(Local_MarkerLayer); + markerWidget->setNeedMouseFocus(true); markerWidget->setUserString("ToolTipType", "Layout"); markerWidget->setUserString("ToolTipLayout", "TextToolTipOneLine"); markerWidget->setUserString("Caption_TextOneLine", marker.name); - markerWidget->eventMouseSetFocus += MyGUI::newDelegate(this, &LocalMapBase::onMarkerFocused); - markerWidget->eventMouseLostFocus += MyGUI::newDelegate(this, &LocalMapBase::onMarkerUnfocused); // Used by tooltips to not show the tooltip if marker is hidden by fog of war markerWidget->setUserString("IsMarker", "true"); markerWidget->setUserData(markerPos); + doorMarkerCreated(markerWidget); mDoorMarkerWidgets.push_back(markerWidget); } - updateMarkers(); - - applyFogOfWar(); - - // set the compass texture again, because MyGUI determines sorting of ImageBox widgets - // based on the last setImageTexture call - std::string tex = "textures\\compass.dds"; - mCompass->setImageTexture(""); - mCompass->setImageTexture(tex); + updateMagicMarkers(); + updateCustomMarkers(); } - - void LocalMapBase::setPlayerPos(const float x, const float y) + void LocalMapBase::redraw() { - updateMarkers(); - - if (x == mLastPositionX && y == mLastPositionY) - return; + // Redraw children in proper order + mLocalMap->getParent()->_updateChilds(); + } - notifyPlayerUpdate (); + void LocalMapBase::setPlayerPos(int cellX, int cellY, const float nx, const float ny) + { + MyGUI::IntPoint pos(widgetSize+nx*widgetSize-16, widgetSize+ny*widgetSize-16); + pos.left += (cellX - mCurX) * widgetSize; + pos.top -= (cellY - mCurY) * widgetSize; - MyGUI::IntSize size = mLocalMap->getCanvasSize(); - MyGUI::IntPoint middle = MyGUI::IntPoint((1/3.f + x/3.f)*size.width,(1/3.f + y/3.f)*size.height); - MyGUI::IntCoord viewsize = mLocalMap->getCoord(); - MyGUI::IntPoint pos(0.5*viewsize.width - middle.left, 0.5*viewsize.height - middle.top); - mLocalMap->setViewOffset(pos); + if (pos != mCompass->getPosition()) + { + notifyPlayerUpdate (); - mCompass->setPosition(MyGUI::IntPoint(512+x*512-16, 512+y*512-16)); - mLastPositionX = x; - mLastPositionY = y; + mCompass->setPosition(pos); + MyGUI::IntPoint middle (pos.left+16, pos.top+16); + MyGUI::IntCoord viewsize = mLocalMap->getCoord(); + MyGUI::IntPoint viewOffset(0.5*viewsize.width - middle.left, 0.5*viewsize.height - middle.top); + mLocalMap->setViewOffset(viewOffset); + } } void LocalMapBase::setPlayerDir(const float x, const float y) @@ -341,19 +498,32 @@ namespace MWGui ++counter; MyGUI::ImageBox* markerWidget = mLocalMap->createWidget("ImageBox", widgetCoord, MyGUI::Align::Default); + markerWidget->setDepth(Local_MarkerAboveFogLayer); markerWidget->setImageTexture(markerTexture); markerWidget->setUserString("IsMarker", "true"); markerWidget->setUserData(markerPos); markerWidget->setColour(markerColour); + mMagicMarkerWidgets.push_back(markerWidget); + } + } + + void LocalMapBase::onFrame(float dt) + { + mMarkerUpdateTimer += dt; + + if (mMarkerUpdateTimer >= 0.25) + { + mMarkerUpdateTimer = 0; + updateMagicMarkers(); } } - void LocalMapBase::updateMarkers() + void LocalMapBase::updateMagicMarkers() { // clear all previous markers - for (std::vector::iterator it = mMarkerWidgets.begin(); it != mMarkerWidgets.end(); ++it) + for (std::vector::iterator it = mMagicMarkerWidgets.begin(); it != mMagicMarkerWidgets.end(); ++it) MyGUI::Gui::getInstance().destroyWidget(*it); - mMarkerWidgets.clear(); + mMagicMarkerWidgets.clear(); addDetectionMarkers(MWBase::World::Detect_Creature); addDetectionMarkers(MWBase::World::Detect_Key); @@ -373,22 +543,42 @@ namespace MWGui 8, 8); MyGUI::ImageBox* markerWidget = mLocalMap->createWidget("ImageBox", widgetCoord, MyGUI::Align::Default); + markerWidget->setDepth(Local_MarkerAboveFogLayer); markerWidget->setImageTexture("textures\\menu_map_smark.dds"); markerWidget->setUserString("IsMarker", "true"); markerWidget->setUserData(markerPos); - mMarkerWidgets.push_back(markerWidget); + mMagicMarkerWidgets.push_back(markerWidget); } + + redraw(); } // ------------------------------------------------------------------------------------------ - MapWindow::MapWindow(DragAndDrop* drag, const std::string& cacheDir) + MapWindow::MapWindow(CustomMarkerCollection &customMarkers, DragAndDrop* drag, const std::string& cacheDir) : WindowPinnableBase("openmw_map_window.layout") , NoDrop(drag, mMainWidget) + , LocalMapBase(customMarkers) , mGlobal(false) , mGlobalMap(0) , mGlobalMapRender(0) - { + , mEditNoteDialog() + , mEventBoxGlobal(NULL) + , mEventBoxLocal(NULL) + , mGlobalMapImage(NULL) + , mGlobalMapOverlay(NULL) + { + static bool registered = false; + if (!registered) + { + MyGUI::FactoryManager::getInstance().registerFactory("Widget"); + registered = true; + } + + mEditNoteDialog.setVisible(false); + mEditNoteDialog.eventOkClicked += MyGUI::newDelegate(this, &MapWindow::onNoteEditOk); + mEditNoteDialog.eventDeleteClicked += MyGUI::newDelegate(this, &MapWindow::onNoteEditDelete); + setCoord(500,0,320,300); getWidget(mLocalMap, "LocalMap"); @@ -398,6 +588,14 @@ namespace MWGui getWidget(mPlayerArrowLocal, "CompassLocal"); getWidget(mPlayerArrowGlobal, "CompassGlobal"); + mPlayerArrowGlobal->setDepth(Global_CompassLayer); + mPlayerArrowGlobal->setNeedMouseFocus(false); + mGlobalMapImage->setDepth(Global_MapLayer); + mGlobalMapOverlay->setDepth(Global_ExploreOverlayLayer); + + mLastScrollWindowCoordinates = mLocalMap->getCoord(); + mLocalMap->eventChangeCoord += MyGUI::newDelegate(this, &MapWindow::onChangeScrollWindowCoord); + mGlobalMap->setVisible (false); getWidget(mButton, "WorldButton"); @@ -407,17 +605,115 @@ namespace MWGui getWidget(mEventBoxGlobal, "EventBoxGlobal"); mEventBoxGlobal->eventMouseDrag += MyGUI::newDelegate(this, &MapWindow::onMouseDrag); mEventBoxGlobal->eventMouseButtonPressed += MyGUI::newDelegate(this, &MapWindow::onDragStart); + mEventBoxGlobal->setDepth(Global_ExploreOverlayLayer); + getWidget(mEventBoxLocal, "EventBoxLocal"); mEventBoxLocal->eventMouseDrag += MyGUI::newDelegate(this, &MapWindow::onMouseDrag); mEventBoxLocal->eventMouseButtonPressed += MyGUI::newDelegate(this, &MapWindow::onDragStart); + mEventBoxLocal->eventMouseButtonDoubleClick += MyGUI::newDelegate(this, &MapWindow::onMapDoubleClicked); - LocalMapBase::init(mLocalMap, mPlayerArrowLocal, this); + LocalMapBase::init(mLocalMap, mPlayerArrowLocal); + } + + void MapWindow::onNoteEditOk() + { + if (mEditNoteDialog.getDeleteButtonShown()) + mCustomMarkers.updateMarker(mEditingMarker, mEditNoteDialog.getText()); + else + { + mEditingMarker.mNote = mEditNoteDialog.getText(); + mCustomMarkers.addMarker(mEditingMarker); + } + + mEditNoteDialog.setVisible(false); + } + + void MapWindow::onNoteEditDelete() + { + ConfirmationDialog* confirmation = MWBase::Environment::get().getWindowManager()->getConfirmationDialog(); + confirmation->open("#{sDeleteNote}", "#{sYes}", "#{sNo}"); + confirmation->eventCancelClicked.clear(); + confirmation->eventOkClicked.clear(); + confirmation->eventOkClicked += MyGUI::newDelegate(this, &MapWindow::onNoteEditDeleteConfirm); + } + + void MapWindow::onNoteEditDeleteConfirm() + { + mCustomMarkers.deleteMarker(mEditingMarker); + + mEditNoteDialog.setVisible(false); + } + + void MapWindow::onCustomMarkerDoubleClicked(MyGUI::Widget *sender) + { + mEditingMarker = *sender->getUserData(); + mEditNoteDialog.setText(mEditingMarker.mNote); + mEditNoteDialog.showDeleteButton(true); + mEditNoteDialog.setVisible(true); + } + + void MapWindow::onMapDoubleClicked(MyGUI::Widget *sender) + { + MyGUI::IntPoint clickedPos = MyGUI::InputManager::getInstance().getMousePosition(); + + MyGUI::IntPoint widgetPos = clickedPos - mEventBoxLocal->getAbsolutePosition(); + int x = int(widgetPos.left/float(widgetSize))-1; + int y = (int(widgetPos.top/float(widgetSize))-1)*-1; + float nX = widgetPos.left/float(widgetSize) - int(widgetPos.left/float(widgetSize)); + float nY = widgetPos.top/float(widgetSize) - int(widgetPos.top/float(widgetSize)); + x += mCurX; + y += mCurY; + + Ogre::Vector2 worldPos; + if (mInterior) + { + worldPos = MWBase::Environment::get().getWorld()->interiorMapToWorldPosition(nX, nY, x, y); + } + else + { + worldPos.x = (x + nX) * cellSize; + worldPos.y = (y + (1.0-nY)) * cellSize; + } + + mEditingMarker.mWorldX = worldPos.x; + mEditingMarker.mWorldY = worldPos.y; + + mEditingMarker.mCell.mPaged = !mInterior; + if (mInterior) + mEditingMarker.mCell.mWorldspace = LocalMapBase::mPrefix; + else + { + mEditingMarker.mCell.mWorldspace = "sys::default"; + mEditingMarker.mCell.mIndex.mX = x; + mEditingMarker.mCell.mIndex.mY = y; + } + + mEditNoteDialog.setVisible(true); + mEditNoteDialog.showDeleteButton(false); + mEditNoteDialog.setText(""); + } + + void MapWindow::onChangeScrollWindowCoord(MyGUI::Widget* sender) + { + MyGUI::IntCoord currentCoordinates = sender->getCoord(); + + MyGUI::IntPoint currentViewPortCenter = MyGUI::IntPoint(currentCoordinates.width / 2, currentCoordinates.height / 2); + MyGUI::IntPoint lastViewPortCenter = MyGUI::IntPoint(mLastScrollWindowCoordinates.width / 2, mLastScrollWindowCoordinates.height / 2); + MyGUI::IntPoint viewPortCenterDiff = currentViewPortCenter - lastViewPortCenter; + + mLocalMap->setViewOffset(mLocalMap->getViewOffset() + viewPortCenterDiff); + mGlobalMap->setViewOffset(mGlobalMap->getViewOffset() + viewPortCenterDiff); + + mLastScrollWindowCoordinates = currentCoordinates; } void MapWindow::renderGlobalMap(Loading::Listener* loadingListener) { mGlobalMapRender = new MWRender::GlobalMap(""); mGlobalMapRender->render(loadingListener); + mGlobalMap->setCanvasSize (mGlobalMapRender->getWidth(), mGlobalMapRender->getHeight()); + mGlobalMapImage->setSize(mGlobalMapRender->getWidth(), mGlobalMapRender->getHeight()); + mGlobalMapImage->setImageTexture("GlobalMap.png"); mGlobalMapOverlay->setImageTexture("GlobalMapOverlay"); } @@ -434,34 +730,32 @@ namespace MWGui void MapWindow::addVisitedLocation(const std::string& name, int x, int y) { - float worldX, worldY; - mGlobalMapRender->cellTopLeftCornerToImageSpace (x, y, worldX, worldY); - - MyGUI::IntCoord widgetCoord( - worldX * mGlobalMapRender->getWidth()+6, - worldY * mGlobalMapRender->getHeight()+6, - 12, 12); - - static int _counter=0; - MyGUI::Button* markerWidget = mGlobalMapOverlay->createWidget("ButtonImage", - widgetCoord, MyGUI::Align::Default); - markerWidget->setImageResource("DoorMarker"); - markerWidget->setUserString("ToolTipType", "Layout"); - markerWidget->setUserString("ToolTipLayout", "TextToolTipOneLine"); - markerWidget->setUserString("Caption_TextOneLine", name); - ++_counter; - - markerWidget = mEventBoxGlobal->createWidget("", - widgetCoord, MyGUI::Align::Default); - markerWidget->setNeedMouseFocus (true); - markerWidget->setUserString("ToolTipType", "Layout"); - markerWidget->setUserString("ToolTipLayout", "TextToolTipOneLine"); - markerWidget->setUserString("Caption_TextOneLine", name); - CellId cell; cell.first = x; cell.second = y; - mMarkers.push_back(cell); + if (mMarkers.insert(cell).second) + { + float worldX, worldY; + mGlobalMapRender->cellTopLeftCornerToImageSpace (x, y, worldX, worldY); + + int markerSize = 12; + int offset = mGlobalMapRender->getCellSize()/2 - markerSize/2; + MyGUI::IntCoord widgetCoord( + worldX * mGlobalMapRender->getWidth()+offset, + worldY * mGlobalMapRender->getHeight()+offset, + markerSize, markerSize); + + MyGUI::Widget* markerWidget = mGlobalMap->createWidget("MarkerButton", + widgetCoord, MyGUI::Align::Default); + markerWidget->setNeedMouseFocus(true); + markerWidget->setColour(MyGUI::Colour::parse(MyGUI::LanguageManager::getInstance().replaceTags("#{fontcolour=normal}"))); + markerWidget->setUserString("ToolTipType", "Layout"); + markerWidget->setUserString("ToolTipLayout", "TextToolTipOneLine"); + markerWidget->setUserString("Caption_TextOneLine", name); + markerWidget->setDepth(Global_MarkerLayer); + markerWidget->eventMouseDrag += MyGUI::newDelegate(this, &MapWindow::onMouseDrag); + markerWidget->eventMouseButtonPressed += MyGUI::newDelegate(this, &MapWindow::onDragStart); + } } void MapWindow::cellExplored(int x, int y) @@ -471,6 +765,8 @@ namespace MWGui void MapWindow::onFrame(float dt) { + LocalMapBase::onFrame(dt); + for (std::vector::iterator it = mQueuedToExplore.begin(); it != mQueuedToExplore.end(); ++it) { mGlobalMapRender->exploreCell(it->first, it->second); @@ -497,7 +793,6 @@ namespace MWGui else mGlobalMap->setViewOffset( mGlobalMap->getViewOffset() + diff ); - mLastDragPos = MyGUI::IntPoint(_left, _top); } @@ -519,21 +814,15 @@ namespace MWGui MWBase::Environment::get().getWindowManager()->setMinimapVisibility(!mPinned); } - void MapWindow::open() + void MapWindow::onTitleDoubleClicked() { - mGlobalMap->setCanvasSize (mGlobalMapRender->getWidth(), mGlobalMapRender->getHeight()); - mGlobalMapImage->setSize(mGlobalMapRender->getWidth(), mGlobalMapRender->getHeight()); - - // force markers to foreground - for (unsigned int i=0; igetChildCount (); ++i) - { - if (mGlobalMapOverlay->getChildAt (i)->getName().substr(0,4) == "Door") - mGlobalMapOverlay->getChildAt (i)->castType()->setImageResource("DoorMarker"); - } + if (!mPinned) + MWBase::Environment::get().getWindowManager()->toggleVisible(GW_Map); + } + void MapWindow::open() + { globalMapUpdatePlayer(); - - mPlayerArrowGlobal->setImageTexture ("textures\\compass.dds"); } void MapWindow::globalMapUpdatePlayer () @@ -542,8 +831,6 @@ namespace MWGui if (MWBase::Environment::get().getWorld ()->isCellExterior ()) { Ogre::Vector3 pos = MWBase::Environment::get().getWorld ()->getPlayerPtr().getRefData ().getBaseNode ()->_getDerivedPosition (); - Ogre::Quaternion orient = MWBase::Environment::get().getWorld ()->getPlayerPtr().getRefData ().getBaseNode ()->_getDerivedOrientation (); - Ogre::Vector2 dir (orient.yAxis ().x, orient.yAxis().y); float worldX, worldY; mGlobalMapRender->worldPosToImageSpace (pos.x, pos.y, worldX, worldY); @@ -552,12 +839,6 @@ namespace MWGui mPlayerArrowGlobal->setPosition(MyGUI::IntPoint(worldX - 16, worldY - 16)); - MyGUI::ISubWidget* main = mPlayerArrowGlobal->getSubWidgetMain(); - MyGUI::RotatingSkin* rotatingSubskin = main->castType(); - rotatingSubskin->setCenter(MyGUI::IntPoint(16,16)); - float angle = std::atan2(dir.x, dir.y); - rotatingSubskin->setAngle(angle); - // set the view offset so that player is in the center MyGUI::IntSize viewsize = mGlobalMap->getSize(); MyGUI::IntPoint viewoffs(0.5*viewsize.width - worldX, 0.5*viewsize.height - worldY); @@ -570,20 +851,6 @@ namespace MWGui globalMapUpdatePlayer (); } - void MapWindow::notifyMapChanged () - { - // workaround to prevent the map from drawing on top of the button - MyGUI::IntCoord oldCoord = mButton->getCoord (); - MyGUI::Gui::getInstance().destroyWidget (mButton); - mButton = mMainWidget->createWidget("MW_Button", - oldCoord, MyGUI::Align::Bottom | MyGUI::Align::Right); - mButton->setProperty ("ExpandDirection", "Left"); - - mButton->eventMouseButtonClick += MyGUI::newDelegate(this, &MapWindow::onWorldButtonClicked); - mButton->setCaptionWithReplacing( mGlobal ? "#{sLocal}" : - "#{sWorld}"); - } - void MapWindow::setGlobalMapPlayerPosition(float worldX, float worldY) { float x, y; @@ -599,15 +866,23 @@ namespace MWGui mGlobalMap->setViewOffset(viewoffs); } + void MapWindow::setGlobalMapPlayerDir(const float x, const float y) + { + MyGUI::ISubWidget* main = mPlayerArrowGlobal->getSubWidgetMain(); + MyGUI::RotatingSkin* rotatingSubskin = main->castType(); + rotatingSubskin->setCenter(MyGUI::IntPoint(16,16)); + float angle = std::atan2(x,y); + rotatingSubskin->setAngle(angle); + } + void MapWindow::clear() { mMarkers.clear(); mGlobalMapRender->clear(); + mChanged = true; while (mEventBoxGlobal->getChildCount()) MyGUI::Gui::getInstance().destroyWidget(mEventBoxGlobal->getChildAt(0)); - while (mGlobalMapOverlay->getChildCount()) - MyGUI::Gui::getInstance().destroyWidget(mGlobalMapOverlay->getChildAt(0)); } void MapWindow::write(ESM::ESMWriter &writer, Loading::Listener& progress) @@ -632,7 +907,7 @@ namespace MWGui mGlobalMapRender->read(map); - for (std::vector::iterator it = map.mMarkers.begin(); it != map.mMarkers.end(); ++it) + for (std::set::iterator it = map.mMarkers.begin(); it != map.mMarkers.end(); ++it) { const ESM::Cell* cell = MWBase::Environment::get().getWorld()->getStore().get().search(it->first, it->second); if (cell && !cell->mName.empty()) @@ -640,4 +915,89 @@ namespace MWGui } } } + + void MapWindow::setAlpha(float alpha) + { + NoDrop::setAlpha(alpha); + // can't allow showing map with partial transparency, as the fog of war will also go transparent + // and reveal parts of the map you shouldn't be able to see + for (std::vector::iterator it = mMapWidgets.begin(); it != mMapWidgets.end(); ++it) + (*it)->setVisible(alpha == 1); + } + + void MapWindow::customMarkerCreated(MyGUI::Widget *marker) + { + marker->eventMouseDrag += MyGUI::newDelegate(this, &MapWindow::onMouseDrag); + marker->eventMouseButtonPressed += MyGUI::newDelegate(this, &MapWindow::onDragStart); + marker->eventMouseButtonDoubleClick += MyGUI::newDelegate(this, &MapWindow::onCustomMarkerDoubleClicked); + } + + void MapWindow::doorMarkerCreated(MyGUI::Widget *marker) + { + marker->eventMouseDrag += MyGUI::newDelegate(this, &MapWindow::onMouseDrag); + marker->eventMouseButtonPressed += MyGUI::newDelegate(this, &MapWindow::onDragStart); + } + + // ------------------------------------------------------------------- + + EditNoteDialog::EditNoteDialog() + : WindowModal("openmw_edit_note.layout") + { + getWidget(mOkButton, "OkButton"); + getWidget(mCancelButton, "CancelButton"); + getWidget(mDeleteButton, "DeleteButton"); + getWidget(mTextEdit, "TextEdit"); + + mCancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &EditNoteDialog::onCancelButtonClicked); + mOkButton->eventMouseButtonClick += MyGUI::newDelegate(this, &EditNoteDialog::onOkButtonClicked); + mDeleteButton->eventMouseButtonClick += MyGUI::newDelegate(this, &EditNoteDialog::onDeleteButtonClicked); + } + + void EditNoteDialog::showDeleteButton(bool show) + { + mDeleteButton->setVisible(show); + } + + bool EditNoteDialog::getDeleteButtonShown() + { + return mDeleteButton->getVisible(); + } + + void EditNoteDialog::setText(const std::string &text) + { + mTextEdit->setCaption(MyGUI::TextIterator::toTagsString(text)); + } + + std::string EditNoteDialog::getText() + { + return MyGUI::TextIterator::getOnlyText(mTextEdit->getCaption()); + } + + void EditNoteDialog::open() + { + WindowModal::open(); + center(); + MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mTextEdit); + } + + void EditNoteDialog::exit() + { + setVisible(false); + } + + void EditNoteDialog::onCancelButtonClicked(MyGUI::Widget *sender) + { + setVisible(false); + } + + void EditNoteDialog::onOkButtonClicked(MyGUI::Widget *sender) + { + eventOkClicked(); + } + + void EditNoteDialog::onDeleteButtonClicked(MyGUI::Widget *sender) + { + eventDeleteClicked(); + } + } diff --git a/apps/openmw/mwgui/mapwindow.hpp b/apps/openmw/mwgui/mapwindow.hpp index d23b0c228..a8aaa8e45 100644 --- a/apps/openmw/mwgui/mapwindow.hpp +++ b/apps/openmw/mwgui/mapwindow.hpp @@ -5,6 +5,8 @@ #include "windowpinnablebase.hpp" +#include + namespace MWRender { class GlobalMap; @@ -23,17 +25,59 @@ namespace Loading namespace MWGui { + + struct CustomMarker + { + float mWorldX; + float mWorldY; + + ESM::CellId mCell; + + std::string mNote; + + bool operator == (const CustomMarker& other) + { + return mNote == other.mNote && mCell == other.mCell && mWorldX == other.mWorldX && mWorldY == other.mWorldY; + } + + void load (ESM::ESMReader& reader); + void save (ESM::ESMWriter& writer) const; + }; + + class CustomMarkerCollection + { + public: + void addMarker(const CustomMarker& marker, bool triggerEvent=true); + void deleteMarker (const CustomMarker& marker); + void updateMarker(const CustomMarker& marker, const std::string& newNote); + + void clear(); + + size_t size() const; + + std::vector::const_iterator begin() const; + std::vector::const_iterator end() const; + + typedef MyGUI::delegates::CMultiDelegate0 EventHandle_Void; + EventHandle_Void eventMarkersChanged; + + private: + std::vector mMarkers; + }; + class LocalMapBase { public: - LocalMapBase(); + LocalMapBase(CustomMarkerCollection& markers); virtual ~LocalMapBase(); - void init(MyGUI::ScrollView* widget, MyGUI::ImageBox* compass, OEngine::GUI::Layout* layout, bool mapDragAndDrop=false); + void init(MyGUI::ScrollView* widget, MyGUI::ImageBox* compass); void setCellPrefix(const std::string& prefix); void setActiveCell(const int x, const int y, bool interior=false); void setPlayerDir(const float x, const float y); - void setPlayerPos(const float x, const float y); + void setPlayerPos(int cellX, int cellY, const float nx, const float ny); + + void onFrame(float dt); bool toggleFogOfWar(); @@ -55,46 +99,79 @@ namespace MWGui bool mChanged; bool mFogOfWar; + // Stores markers that were placed by a player. May be shared between multiple map views. + CustomMarkerCollection& mCustomMarkers; + std::vector mMapWidgets; std::vector mFogWidgets; // Keep track of created marker widgets, just to easily remove them later. - std::vector mDoorMarkerWidgets; // Doors - std::vector mMarkerWidgets; // Other markers + std::vector mDoorMarkerWidgets; + std::vector mMagicMarkerWidgets; + std::vector mCustomMarkerWidgets; - void applyFogOfWar(); + void updateCustomMarkers(); - void onMarkerFocused(MyGUI::Widget* w1, MyGUI::Widget* w2); - void onMarkerUnfocused(MyGUI::Widget* w1, MyGUI::Widget* w2); + void applyFogOfWar(); MyGUI::IntPoint getMarkerPosition (float worldX, float worldY, MarkerPosition& markerPos); virtual void notifyPlayerUpdate() {} virtual void notifyMapChanged() {} - // Update markers (Detect X effects, Mark/Recall effects) - // Note, door markers handled in setActiveCell - void updateMarkers(); + virtual void customMarkerCreated(MyGUI::Widget* marker) {} + virtual void doorMarkerCreated(MyGUI::Widget* marker) {} + + void updateMagicMarkers(); void addDetectionMarkers(int type); - OEngine::GUI::Layout* mLayout; + void redraw(); - bool mMapDragAndDrop; + float mMarkerUpdateTimer; - float mLastPositionX; - float mLastPositionY; float mLastDirectionX; float mLastDirectionY; }; + class EditNoteDialog : public MWGui::WindowModal + { + public: + EditNoteDialog(); + + virtual void open(); + virtual void exit(); + + void showDeleteButton(bool show); + bool getDeleteButtonShown(); + void setText(const std::string& text); + std::string getText(); + + typedef MyGUI::delegates::CMultiDelegate0 EventHandle_Void; + + EventHandle_Void eventDeleteClicked; + EventHandle_Void eventOkClicked; + + private: + void onCancelButtonClicked(MyGUI::Widget* sender); + void onOkButtonClicked(MyGUI::Widget* sender); + void onDeleteButtonClicked(MyGUI::Widget* sender); + + MyGUI::TextBox* mTextEdit; + MyGUI::Button* mOkButton; + MyGUI::Button* mCancelButton; + MyGUI::Button* mDeleteButton; + }; + class MapWindow : public MWGui::WindowPinnableBase, public LocalMapBase, public NoDrop { public: - MapWindow(DragAndDrop* drag, const std::string& cacheDir); + MapWindow(CustomMarkerCollection& customMarkers, DragAndDrop* drag, const std::string& cacheDir); virtual ~MapWindow(); void setCellName(const std::string& cellName); + virtual void setAlpha(float alpha); + void renderGlobalMap(Loading::Listener* loadingListener); // adds the marker to the global map @@ -104,6 +181,7 @@ namespace MWGui void cellExplored(int x, int y); void setGlobalMapPlayerPosition (float worldX, float worldY); + void setGlobalMapPlayerDir(const float x, const float y); virtual void open(); @@ -119,7 +197,13 @@ namespace MWGui void onDragStart(MyGUI::Widget* _sender, int _left, int _top, MyGUI::MouseButton _id); void onMouseDrag(MyGUI::Widget* _sender, int _left, int _top, MyGUI::MouseButton _id); void onWorldButtonClicked(MyGUI::Widget* _sender); - + void onMapDoubleClicked(MyGUI::Widget* sender); + void onCustomMarkerDoubleClicked(MyGUI::Widget* sender); + void onNoteEditOk(); + void onNoteEditDelete(); + void onNoteEditDeleteConfirm(); + void onNoteDoubleClicked(MyGUI::Widget* sender); + void onChangeScrollWindowCoord(MyGUI::Widget* sender); void globalMapUpdatePlayer(); MyGUI::ScrollView* mGlobalMap; @@ -131,9 +215,11 @@ namespace MWGui MyGUI::IntPoint mLastDragPos; bool mGlobal; + MyGUI::IntCoord mLastScrollWindowCoordinates; + // Markers on global map typedef std::pair CellId; - std::vector mMarkers; + std::set mMarkers; // Cells that should be explored in the next frame (i.e. their map revealed on the global map) // We can't do this immediately, because the map update is not immediate either (see mNeedMapUpdate in scene.cpp) @@ -144,11 +230,16 @@ namespace MWGui MWRender::GlobalMap* mGlobalMapRender; - protected: + EditNoteDialog mEditNoteDialog; + CustomMarker mEditingMarker; + virtual void onPinToggled(); + virtual void onTitleDoubleClicked(); + + virtual void doorMarkerCreated(MyGUI::Widget* marker); + virtual void customMarkerCreated(MyGUI::Widget *marker); virtual void notifyPlayerUpdate(); - virtual void notifyMapChanged(); }; } diff --git a/apps/openmw/mwgui/merchantrepair.cpp b/apps/openmw/mwgui/merchantrepair.cpp index 50e7644fb..e85681c04 100644 --- a/apps/openmw/mwgui/merchantrepair.cpp +++ b/apps/openmw/mwgui/merchantrepair.cpp @@ -10,6 +10,8 @@ #include "../mwbase/windowmanager.hpp" #include "../mwbase/soundmanager.hpp" +#include "../mwmechanics/creaturestats.hpp" + #include "../mwworld/class.hpp" #include "../mwworld/containerstore.hpp" #include "../mwworld/esmstore.hpp" @@ -67,11 +69,11 @@ void MerchantRepair::startRepair(const MWWorld::Ptr &actor) std::string name = iter->getClass().getName(*iter) + " - " + boost::lexical_cast(price) + MWBase::Environment::get().getWorld()->getStore().get() - .find("sgp")->getString();; + .find("sgp")->getString(); MyGUI::Button* button = - mList->createWidget("SandTextButton", + mList->createWidget(price <= playerGold ? "SandTextButton" : "SandTextButtonDisabled", // can't use setEnabled since that removes tooltip 0, currentY, 0, @@ -81,7 +83,6 @@ void MerchantRepair::startRepair(const MWWorld::Ptr &actor) currentY += 18; - button->setEnabled(price<=playerGold); button->setUserString("Price", boost::lexical_cast(price)); button->setUserData(*iter); button->setCaptionWithReplacing(name); @@ -91,7 +92,10 @@ void MerchantRepair::startRepair(const MWWorld::Ptr &actor) button->eventMouseButtonClick += MyGUI::newDelegate(this, &MerchantRepair::onRepairButtonClick); } } + // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the scrollbar is hidden + mList->setVisibleVScroll(false); mList->setCanvasSize (MyGUI::IntSize(mList->getWidth(), std::max(mList->getHeight(), currentY))); + mList->setVisibleVScroll(true); mGoldLabel->setCaptionWithReplacing("#{sGold}: " + boost::lexical_cast(playerGold)); @@ -110,25 +114,40 @@ void MerchantRepair::open() center(); } +void MerchantRepair::exit() +{ + MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_MerchantRepair); +} + void MerchantRepair::onRepairButtonClick(MyGUI::Widget *sender) { + MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); + + int price = boost::lexical_cast(sender->getUserString("Price")); + if (price > player.getClass().getContainerStore(player).count(MWWorld::ContainerStore::sGoldId)) + return; + // repair MWWorld::Ptr item = *sender->getUserData(); item.getCellRef().setCharge(item.getClass().getItemMaxHealth(item)); + player.getClass().getContainerStore(player).restack(item); + MWBase::Environment::get().getSoundManager()->playSound("Repair",1,1); - int price = boost::lexical_cast(sender->getUserString("Price")); - MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); player.getClass().getContainerStore(player).remove(MWWorld::ContainerStore::sGoldId, price, player); + // add gold to NPC trading gold pool + MWMechanics::CreatureStats& actorStats = mActor.getClass().getCreatureStats(mActor); + actorStats.setGoldPool(actorStats.getGoldPool() + price); + startRepair(mActor); } void MerchantRepair::onOkButtonClick(MyGUI::Widget *sender) { - MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_MerchantRepair); + exit(); } } diff --git a/apps/openmw/mwgui/merchantrepair.hpp b/apps/openmw/mwgui/merchantrepair.hpp index 4cb39fe01..2f1387365 100644 --- a/apps/openmw/mwgui/merchantrepair.hpp +++ b/apps/openmw/mwgui/merchantrepair.hpp @@ -16,6 +16,8 @@ public: virtual void open(); + virtual void exit(); + void startRepair(const MWWorld::Ptr& actor); private: diff --git a/apps/openmw/mwgui/messagebox.cpp b/apps/openmw/mwgui/messagebox.cpp index 1ce167c33..d4520aad7 100644 --- a/apps/openmw/mwgui/messagebox.cpp +++ b/apps/openmw/mwgui/messagebox.cpp @@ -25,6 +25,23 @@ namespace MWGui } } + void MessageBoxManager::clear() + { + delete mInterMessageBoxe; + mInterMessageBoxe = NULL; + + std::vector::iterator it(mMessageBoxes.begin()); + for (; it != mMessageBoxes.end(); ++it) + { + if (*it == mStaticMessageBox) + mStaticMessageBox = NULL; + delete *it; + } + mMessageBoxes.clear(); + + mLastButtonPressed = -1; + } + void MessageBoxManager::onFrame (float frameDuration) { std::vector::iterator it; @@ -122,11 +139,6 @@ namespace MWGui return false; } - void MessageBoxManager::setMessageBoxSpeed (int speed) - { - mMessageBoxSpeed = speed; - } - int MessageBoxManager::readPressedButton () { int pressed = mLastButtonPressed; @@ -145,12 +157,11 @@ namespace MWGui , mMaxTime(0) { // defines - mBottomPadding = 20; - mNextBoxPadding = 20; + mBottomPadding = 48; + mNextBoxPadding = 4; getWidget(mMessageWidget, "message"); - mMessageWidget->setOverflowToTheLeft(true); mMessageWidget->setCaptionWithReplacing(mMessage); } @@ -166,7 +177,7 @@ namespace MWGui int MessageBox::getHeight () { - return mMainWidget->getHeight()+mNextBoxPadding; // 20 is the padding between this and the next MessageBox + return mMainWidget->getHeight()+mNextBoxPadding; } @@ -179,11 +190,10 @@ namespace MWGui { WindowModal::open(); - int fixedWidth = 500; int textPadding = 10; // padding between text-widget and main-widget - int textButtonPadding = 20; // padding between the text-widget und the button-widget + int textButtonPadding = 10; // padding between the text-widget und the button-widget int buttonLeftPadding = 10; // padding between the buttons if horizontal - int buttonTopPadding = 5; // ^-- if vertical + int buttonTopPadding = 10; // ^-- if vertical int buttonPadding = 5; // padding between button label and button itself int buttonMainPadding = 10; // padding between buttons and bottom of the main widget @@ -193,7 +203,7 @@ namespace MWGui getWidget(mMessageWidget, "message"); getWidget(mButtonsWidget, "buttons"); - mMessageWidget->setOverflowToTheLeft(true); + mMessageWidget->setSize(400, mMessageWidget->getHeight()); mMessageWidget->setCaptionWithReplacing(message); MyGUI::IntSize textSize = mMessageWidget->getTextSize(); @@ -201,8 +211,8 @@ namespace MWGui MyGUI::IntSize gameWindowSize = MyGUI::RenderManager::getInstance().getViewSize(); int biggestButtonWidth = 0; - int buttonWidth = 0; int buttonsWidth = 0; + int buttonsHeight = 0; int buttonHeight = 0; MyGUI::IntCoord dummyCoord(0, 0, 0, 0); @@ -220,37 +230,41 @@ namespace MWGui mButtons.push_back(button); - buttonWidth = button->getTextSize().width + 2*buttonPadding + buttonLeftPadding; + if (buttonsWidth != 0) + buttonsWidth += buttonLeftPadding; + + int buttonWidth = button->getTextSize().width + 2*buttonPadding; buttonsWidth += buttonWidth; - buttonHeight = button->getTextSize().height + 2*buttonPadding + buttonTopPadding; + + buttonHeight = button->getTextSize().height + 2*buttonPadding; + + if (buttonsHeight != 0) + buttonsHeight += buttonTopPadding; + buttonsHeight += buttonHeight; if(buttonWidth > biggestButtonWidth) { biggestButtonWidth = buttonWidth; } } - buttonsWidth += buttonLeftPadding; MyGUI::IntSize mainWidgetSize; - if(buttonsWidth < fixedWidth) + if(buttonsWidth < textSize.width) { // on one line - if(textSize.width + 2*textPadding < buttonsWidth) - { - mainWidgetSize.width = buttonsWidth; - } - else - { - mainWidgetSize.width = textSize.width + 3*textPadding; - } - mainWidgetSize.height = textSize.height + textButtonPadding + buttonHeight + buttonMainPadding; + mainWidgetSize.width = textSize.width + 3*textPadding; + mainWidgetSize.height = textPadding + textSize.height + textButtonPadding + buttonHeight + buttonMainPadding; + + MyGUI::IntSize realSize = mainWidgetSize + + // To account for borders + (mMainWidget->getSize() - mMainWidget->getClientWidget()->getSize()); MyGUI::IntPoint absPos; - absPos.left = (gameWindowSize.width - mainWidgetSize.width)/2; - absPos.top = (gameWindowSize.height - mainWidgetSize.height)/2; + absPos.left = (gameWindowSize.width - realSize.width)/2; + absPos.top = (gameWindowSize.height - realSize.height)/2; mMainWidget->setPosition(absPos); - mMainWidget->setSize(mainWidgetSize); + mMainWidget->setSize(realSize); MyGUI::IntCoord messageWidgetCoord; messageWidgetCoord.left = (mainWidgetSize.width - textSize.width)/2; @@ -261,13 +275,13 @@ namespace MWGui MyGUI::IntCoord buttonCord; MyGUI::IntSize buttonSize(0, buttonHeight); - int left = (mainWidgetSize.width - buttonsWidth)/2 + buttonPadding; + int left = (mainWidgetSize.width - buttonsWidth)/2; std::vector::const_iterator button; for(button = mButtons.begin(); button != mButtons.end(); ++button) { buttonCord.left = left; - buttonCord.top = textSize.height + textButtonPadding; + buttonCord.top = messageWidgetCoord.top + textSize.height + textButtonPadding; buttonSize.width = (*button)->getTextSize().width + 2*buttonPadding; buttonSize.height = (*button)->getTextSize().height + 2*buttonPadding; @@ -282,7 +296,7 @@ namespace MWGui { // among each other if(biggestButtonWidth > textSize.width) { - mainWidgetSize.width = biggestButtonWidth + buttonTopPadding; + mainWidgetSize.width = biggestButtonWidth + buttonTopPadding*2; } else { mainWidgetSize.width = textSize.width + 3*textPadding; @@ -291,7 +305,7 @@ namespace MWGui MyGUI::IntCoord buttonCord; MyGUI::IntSize buttonSize(0, buttonHeight); - int top = textButtonPadding + buttonTopPadding + textSize.height; + int top = textPadding + textSize.height + textButtonPadding; std::vector::const_iterator button; for(button = mButtons.begin(); button != mButtons.end(); ++button) @@ -300,16 +314,18 @@ namespace MWGui buttonSize.height = (*button)->getTextSize().height + buttonPadding*2; buttonCord.top = top; - buttonCord.left = (mainWidgetSize.width - buttonSize.width)/2 - 5; // FIXME: -5 is not so nice :/ + buttonCord.left = (mainWidgetSize.width - buttonSize.width)/2; (*button)->setCoord(buttonCord); (*button)->setSize(buttonSize); - top += buttonSize.height + 2*buttonTopPadding; + top += buttonSize.height + buttonTopPadding; } - mainWidgetSize.height = top + buttonMainPadding; - mMainWidget->setSize(mainWidgetSize); + mainWidgetSize.height = textPadding + textSize.height + textButtonPadding + buttonsHeight + buttonMainPadding; + mMainWidget->setSize(mainWidgetSize + + // To account for borders + (mMainWidget->getSize() - mMainWidget->getClientWidget()->getSize())); MyGUI::IntPoint absPos; absPos.left = (gameWindowSize.width - mainWidgetSize.width)/2; diff --git a/apps/openmw/mwgui/messagebox.hpp b/apps/openmw/mwgui/messagebox.hpp index caa37008c..5f180df20 100644 --- a/apps/openmw/mwgui/messagebox.hpp +++ b/apps/openmw/mwgui/messagebox.hpp @@ -30,8 +30,10 @@ namespace MWGui bool createInteractiveMessageBox (const std::string& message, const std::vector& buttons); bool isInteractiveMessageBox (); + /// Remove all message boxes + void clear(); + bool removeMessageBox (MessageBox *msgbox); - void setMessageBoxSpeed (int speed); int readPressedButton (); diff --git a/apps/openmw/mwgui/pickpocketitemmodel.cpp b/apps/openmw/mwgui/pickpocketitemmodel.cpp index 230282f15..f6882ada6 100644 --- a/apps/openmw/mwgui/pickpocketitemmodel.cpp +++ b/apps/openmw/mwgui/pickpocketitemmodel.cpp @@ -6,16 +6,19 @@ namespace MWGui { - PickpocketItemModel::PickpocketItemModel(const MWWorld::Ptr& thief, ItemModel *sourceModel) + PickpocketItemModel::PickpocketItemModel(const MWWorld::Ptr& thief, ItemModel *sourceModel, bool hideItems) { mSourceModel = sourceModel; int chance = thief.getClass().getSkill(thief, ESM::Skill::Sneak); mSourceModel->update(); - for (size_t i = 0; igetItemCount(); ++i) + if (hideItems) { - if (std::rand() / static_cast(RAND_MAX) * 100 > chance) - mHiddenItems.push_back(mSourceModel->getItem(i)); + for (size_t i = 0; igetItemCount(); ++i) + { + if (std::rand() / static_cast(RAND_MAX) * 100 > chance) + mHiddenItems.push_back(mSourceModel->getItem(i)); + } } } @@ -42,11 +45,8 @@ namespace MWGui const ItemStack& item = mSourceModel->getItem(i); // Bound items may not be stolen - if (item.mBase.getCellRef().getRefId().size() > 6 - && item.mBase.getCellRef().getRefId().substr(0,6) == "bound_") - { + if (item.mFlags & ItemStack::Flag_Bound) continue; - } if (std::find(mHiddenItems.begin(), mHiddenItems.end(), item) == mHiddenItems.end() && item.mType != ItemStack::Type_Equipped) diff --git a/apps/openmw/mwgui/pickpocketitemmodel.hpp b/apps/openmw/mwgui/pickpocketitemmodel.hpp index 090d48d0e..af72119c8 100644 --- a/apps/openmw/mwgui/pickpocketitemmodel.hpp +++ b/apps/openmw/mwgui/pickpocketitemmodel.hpp @@ -10,7 +10,7 @@ namespace MWGui class PickpocketItemModel : public ProxyItemModel { public: - PickpocketItemModel (const MWWorld::Ptr& thief, ItemModel* sourceModel); + PickpocketItemModel (const MWWorld::Ptr& thief, ItemModel* sourceModel, bool hideItems=true); virtual ItemStack getItem (ModelIndex index); virtual size_t getItemCount(); virtual void update(); diff --git a/apps/openmw/mwgui/quickkeysmenu.cpp b/apps/openmw/mwgui/quickkeysmenu.cpp index 90abbb145..440165e74 100644 --- a/apps/openmw/mwgui/quickkeysmenu.cpp +++ b/apps/openmw/mwgui/quickkeysmenu.cpp @@ -3,9 +3,11 @@ #include #include +#include #include "../mwworld/inventorystore.hpp" #include "../mwworld/class.hpp" +#include "../mwworld/player.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" @@ -23,6 +25,9 @@ #include "spellwindow.hpp" +#include "itemwidget.hpp" +#include "sortfilteritemmodel.hpp" + namespace MWGui { @@ -46,17 +51,24 @@ namespace MWGui for (int i = 0; i < 10; ++i) { - MyGUI::Button* button; + ItemWidget* button; getWidget(button, "QuickKey" + boost::lexical_cast(i+1)); button->eventMouseButtonClick += MyGUI::newDelegate(this, &QuickKeysMenu::onQuickKeyButtonClicked); - unassign(button, i); - mQuickKeyButtons.push_back(button); + + mAssigned.push_back(Type_Unassigned); + + unassign(button, i); } } + void QuickKeysMenu::exit() + { + MWBase::Environment::get().getWindowManager()->removeGuiMode (MWGui::GM_QuickKeysMenu); + } + void QuickKeysMenu::clear() { for (int i=0; i<10; ++i) @@ -72,12 +84,14 @@ namespace MWGui delete mMagicSelectionDialog; } - void QuickKeysMenu::unassign(MyGUI::Widget* key, int index) + void QuickKeysMenu::unassign(ItemWidget* key, int index) { - while (key->getChildCount ()) - MyGUI::Gui::getInstance ().destroyWidget (key->getChildAt(0)); + key->clearUserStrings(); + key->setItem(MWWorld::Ptr()); + while (key->getChildCount()) // Destroy number label + MyGUI::Gui::getInstance().destroyWidget(key->getChildAt(0)); - key->setUserData(Type_Unassigned); + mAssigned[index] = Type_Unassigned; MyGUI::TextBox* textBox = key->createWidgetReal("SandText", MyGUI::FloatCoord(0,0,1,1), MyGUI::Align::Default); textBox->setTextAlign (MyGUI::Align::Center); @@ -123,6 +137,7 @@ namespace MWGui } mItemSelectionDialog->setVisible(true); mItemSelectionDialog->openContainer(MWBase::Environment::get().getWorld()->getPlayerPtr()); + mItemSelectionDialog->setFilter(SortFilterItemModel::Filter_OnlyUsableItems); mAssignDialog->setVisible (false); } @@ -151,27 +166,16 @@ namespace MWGui void QuickKeysMenu::onAssignItem(MWWorld::Ptr item) { - MyGUI::Button* button = mQuickKeyButtons[mSelectedIndex]; - while (button->getChildCount ()) - MyGUI::Gui::getInstance ().destroyWidget (button->getChildAt(0)); - - button->setUserData(Type_Item); - - MyGUI::ImageBox* frame = button->createWidget("ImageBox", MyGUI::IntCoord(9, 8, 42, 42), MyGUI::Align::Default); - std::string backgroundTex = "textures\\menu_icon_barter.dds"; - frame->setImageTexture (backgroundTex); - frame->setImageCoord (MyGUI::IntCoord(4, 4, 40, 40)); - frame->setUserString ("ToolTipType", "ItemPtr"); - frame->setUserData(item); - frame->eventMouseButtonClick += MyGUI::newDelegate(this, &QuickKeysMenu::onQuickKeyButtonClicked); - MyGUI::ImageBox* image = frame->createWidget("ImageBox", MyGUI::IntCoord(5, 5, 32, 32), MyGUI::Align::Default); - std::string path = std::string("icons\\"); - path += item.getClass().getInventoryIcon(item); - int pos = path.rfind("."); - path.erase(pos); - path.append(".dds"); - image->setImageTexture (path); - image->setNeedMouseFocus (false); + assert (mSelectedIndex >= 0); + ItemWidget* button = mQuickKeyButtons[mSelectedIndex]; + while (button->getChildCount()) // Destroy number label + MyGUI::Gui::getInstance().destroyWidget(button->getChildAt(0)); + + mAssigned[mSelectedIndex] = Type_Item; + + button->setItem(item, ItemWidget::Barter); + button->setUserString ("ToolTipType", "ItemPtr"); + button->setUserData(item); if (mItemSelectionDialog) mItemSelectionDialog->setVisible(false); @@ -184,28 +188,18 @@ namespace MWGui void QuickKeysMenu::onAssignMagicItem (MWWorld::Ptr item) { - MyGUI::Button* button = mQuickKeyButtons[mSelectedIndex]; - while (button->getChildCount ()) - MyGUI::Gui::getInstance ().destroyWidget (button->getChildAt(0)); - - button->setUserData(Type_MagicItem); - - MyGUI::ImageBox* frame = button->createWidget("ImageBox", MyGUI::IntCoord(9, 8, 42, 42), MyGUI::Align::Default); - std::string backgroundTex = "textures\\menu_icon_select_magic_magic.dds"; - frame->setImageTexture (backgroundTex); - frame->setImageCoord (MyGUI::IntCoord(2, 2, 40, 40)); - frame->setUserString ("ToolTipType", "ItemPtr"); - frame->setUserData(item); - frame->eventMouseButtonClick += MyGUI::newDelegate(this, &QuickKeysMenu::onQuickKeyButtonClicked); - - MyGUI::ImageBox* image = frame->createWidget("ImageBox", MyGUI::IntCoord(5, 5, 32, 32), MyGUI::Align::Default); - std::string path = std::string("icons\\"); - path += item.getClass().getInventoryIcon(item); - int pos = path.rfind("."); - path.erase(pos); - path.append(".dds"); - image->setImageTexture (path); - image->setNeedMouseFocus (false); + assert (mSelectedIndex >= 0); + ItemWidget* button = mQuickKeyButtons[mSelectedIndex]; + while (button->getChildCount()) // Destroy number label + MyGUI::Gui::getInstance().destroyWidget(button->getChildAt(0)); + + mAssigned[mSelectedIndex] = Type_MagicItem; + + button->setFrame("textures\\menu_icon_select_magic_magic.dds", MyGUI::IntCoord(2, 2, 40, 40)); + button->setIcon(item); + + button->setUserString ("ToolTipType", "ItemPtr"); + button->setUserData(item); if (mMagicSelectionDialog) mMagicSelectionDialog->setVisible(false); @@ -213,21 +207,16 @@ namespace MWGui void QuickKeysMenu::onAssignMagic (const std::string& spellId) { - MyGUI::Button* button = mQuickKeyButtons[mSelectedIndex]; - while (button->getChildCount ()) - MyGUI::Gui::getInstance ().destroyWidget (button->getChildAt(0)); - - button->setUserData(Type_Magic); + assert (mSelectedIndex >= 0); + ItemWidget* button = mQuickKeyButtons[mSelectedIndex]; + while (button->getChildCount()) // Destroy number label + MyGUI::Gui::getInstance().destroyWidget(button->getChildAt(0)); - MyGUI::ImageBox* frame = button->createWidget("ImageBox", MyGUI::IntCoord(9, 8, 42, 42), MyGUI::Align::Default); - std::string backgroundTex = "textures\\menu_icon_select_magic.dds"; - frame->setImageTexture (backgroundTex); - frame->setImageCoord (MyGUI::IntCoord(2, 2, 40, 40)); - frame->setUserString ("ToolTipType", "Spell"); - frame->setUserString ("Spell", spellId); - frame->eventMouseButtonClick += MyGUI::newDelegate(this, &QuickKeysMenu::onQuickKeyButtonClicked); + mAssigned[mSelectedIndex] = Type_Magic; - MyGUI::ImageBox* image = frame->createWidget("ImageBox", MyGUI::IntCoord(5, 5, 32, 32), MyGUI::Align::Default); + button->setItem(MWWorld::Ptr()); + button->setUserString ("ToolTipType", "Spell"); + button->setUserString ("Spell", spellId); const MWWorld::ESMStore &esmStore = MWBase::Environment::get().getWorld()->getStore(); @@ -239,15 +228,12 @@ namespace MWGui esmStore.get().find(spell->mEffects.mList.front().mEffectID); std::string path = effect->mIcon; - int slashPos = path.find("\\"); + int slashPos = path.rfind('\\'); path.insert(slashPos+1, "b_"); - path = std::string("icons\\") + path; - int pos = path.rfind("."); - path.erase(pos); - path.append(".dds"); + path = Misc::ResourceHelpers::correctIconPath(path); - image->setImageTexture (path); - image->setNeedMouseFocus (false); + button->setFrame("textures\\menu_icon_select_magic.dds", MyGUI::IntCoord(2, 2, 40, 40)); + button->setIcon(path); if (mMagicSelectionDialog) mMagicSelectionDialog->setVisible(false); @@ -260,16 +246,17 @@ namespace MWGui void QuickKeysMenu::activateQuickKey(int index) { - MyGUI::Button* button = mQuickKeyButtons[index-1]; + assert (index-1 >= 0); + ItemWidget* button = mQuickKeyButtons[index-1]; - QuickKeyType type = *button->getUserData(); + QuickKeyType type = mAssigned[index-1]; MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); MWWorld::InventoryStore& store = player.getClass().getInventoryStore(player); if (type == Type_Item || type == Type_MagicItem) { - MWWorld::Ptr item = *button->getChildAt (0)->getUserData(); + MWWorld::Ptr item = *button->getUserData(); // make sure the item is available if (item.getRefData ().getCount() < 1) { @@ -281,7 +268,7 @@ namespace MWGui if (Misc::StringUtils::ciEqual(it->getCellRef().getRefId(), id)) { item = *it; - button->getChildAt(0)->setUserData(item); + button->setUserData(item); break; } } @@ -298,7 +285,7 @@ namespace MWGui if (type == Type_Magic) { - std::string spellId = button->getChildAt(0)->getUserString("Spell"); + std::string spellId = button->getUserString("Spell"); // Make sure the player still has this spell MWMechanics::CreatureStats& stats = player.getClass().getCreatureStats(player); @@ -307,16 +294,22 @@ namespace MWGui return; store.setSelectedEnchantItem(store.end()); MWBase::Environment::get().getWindowManager()->setSelectedSpell(spellId, int(MWMechanics::getSpellSuccessChance(spellId, player))); + MWBase::Environment::get().getWorld()->getPlayer().setDrawState(MWMechanics::DrawState_Spell); } else if (type == Type_Item) { - MWWorld::Ptr item = *button->getChildAt (0)->getUserData(); - + MWWorld::Ptr item = *button->getUserData(); MWBase::Environment::get().getWindowManager()->getInventoryWindow()->useItem(item); + MWWorld::ContainerStoreIterator rightHand = store.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); + // change draw state only if the item is in player's right hand + if (rightHand != store.end() && item == *rightHand) + { + MWBase::Environment::get().getWorld()->getPlayer().setDrawState(MWMechanics::DrawState_Weapon); + } } else if (type == Type_MagicItem) { - MWWorld::Ptr item = *button->getChildAt (0)->getUserData(); + MWWorld::Ptr item = *button->getUserData(); // retrieve ContainerStoreIterator to the item MWWorld::ContainerStoreIterator it = store.begin(); @@ -336,6 +329,7 @@ namespace MWGui } store.setSelectedEnchantItem(it); + MWBase::Environment::get().getWorld()->getPlayer().setDrawState(MWMechanics::DrawState_Spell); } } @@ -385,6 +379,11 @@ namespace MWGui center(); } + void QuickKeysMenuAssign::exit() + { + setVisible(false); + } + void QuickKeysMenu::write(ESM::ESMWriter &writer) { writer.startRecord(ESM::REC_KEYS); @@ -393,9 +392,9 @@ namespace MWGui for (int i=0; i<10; ++i) { - MyGUI::Button* button = mQuickKeyButtons[i]; + ItemWidget* button = mQuickKeyButtons[i]; - int type = *button->getUserData(); + int type = mAssigned[i]; ESM::QuickKeys::QuickKey key; key.mType = type; @@ -407,12 +406,12 @@ namespace MWGui case Type_Item: case Type_MagicItem: { - MWWorld::Ptr item = *button->getChildAt(0)->getUserData(); + MWWorld::Ptr item = *button->getUserData(); key.mId = item.getCellRef().getRefId(); break; } case Type_Magic: - std::string spellId = button->getChildAt(0)->getUserString("Spell"); + std::string spellId = button->getUserString("Spell"); key.mId = spellId; break; } @@ -442,7 +441,7 @@ namespace MWGui mSelectedIndex = i; int keyType = it->mType; std::string id = it->mId; - MyGUI::Button* button = mQuickKeyButtons[i]; + ItemWidget* button = mQuickKeyButtons[i]; switch (keyType) { @@ -508,7 +507,12 @@ namespace MWGui void MagicSelectionDialog::onCancelButtonClicked (MyGUI::Widget *sender) { - mParent->onAssignMagicCancel (); + exit(); + } + + void MagicSelectionDialog::exit() + { + mParent->onAssignMagicCancel(); } void MagicSelectionDialog::open () @@ -596,7 +600,7 @@ namespace MWGui for (std::vector::const_iterator it = powers.begin(); it != powers.end(); ++it) { const ESM::Spell* spell = esmStore.get().find(*it); - MyGUI::Button* t = mMagicList->createWidget("SpellText", + MyGUI::Button* t = mMagicList->createWidget("SandTextButton", MyGUI::IntCoord(4, mHeight, mWidth-8, spellHeight), MyGUI::Align::Left | MyGUI::Align::Top); t->setCaption(spell->mName); t->setTextAlign(MyGUI::Align::Left); @@ -613,7 +617,7 @@ namespace MWGui for (std::vector::const_iterator it = spellList.begin(); it != spellList.end(); ++it) { const ESM::Spell* spell = esmStore.get().find(*it); - MyGUI::Button* t = mMagicList->createWidget("SpellText", + MyGUI::Button* t = mMagicList->createWidget("SandTextButton", MyGUI::IntCoord(4, mHeight, mWidth-8, spellHeight), MyGUI::Align::Left | MyGUI::Align::Top); t->setCaption(spell->mName); t->setTextAlign(MyGUI::Align::Left); @@ -644,7 +648,7 @@ namespace MWGui } } - MyGUI::Button* t = mMagicList->createWidget(equipped ? "SpellText" : "SpellTextUnequipped", + MyGUI::Button* t = mMagicList->createWidget(equipped ? "SandTextButton" : "SpellTextUnequipped", MyGUI::IntCoord(4, mHeight, mWidth-8, spellHeight), MyGUI::Align::Left | MyGUI::Align::Top); t->setCaption(item.getClass().getName(item)); t->setTextAlign(MyGUI::Align::Left); @@ -656,9 +660,10 @@ namespace MWGui mHeight += spellHeight; } - + // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the scrollbar is hidden + mMagicList->setVisibleVScroll(false); mMagicList->setCanvasSize (mWidth, std::max(mMagicList->getHeight(), mHeight)); - + mMagicList->setVisibleVScroll(true); } void MagicSelectionDialog::addGroup(const std::string &label, const std::string& label2) diff --git a/apps/openmw/mwgui/quickkeysmenu.hpp b/apps/openmw/mwgui/quickkeysmenu.hpp index c0e25a517..dc088d3c9 100644 --- a/apps/openmw/mwgui/quickkeysmenu.hpp +++ b/apps/openmw/mwgui/quickkeysmenu.hpp @@ -11,6 +11,7 @@ namespace MWGui class QuickKeysMenuAssign; class ItemSelectionDialog; class MagicSelectionDialog; + class ItemWidget; class QuickKeysMenu : public WindowBase { @@ -18,6 +19,7 @@ namespace MWGui QuickKeysMenu(); ~QuickKeysMenu(); + virtual void exit(); void onItemButtonClicked(MyGUI::Widget* sender); void onMagicButtonClicked(MyGUI::Widget* sender); @@ -50,7 +52,8 @@ namespace MWGui MyGUI::EditBox* mInstructionLabel; MyGUI::Button* mOkButton; - std::vector mQuickKeyButtons; + std::vector mQuickKeyButtons; + std::vector mAssigned; QuickKeysMenuAssign* mAssignDialog; ItemSelectionDialog* mItemSelectionDialog; @@ -62,13 +65,14 @@ namespace MWGui void onQuickKeyButtonClicked(MyGUI::Widget* sender); void onOkButtonClicked(MyGUI::Widget* sender); - void unassign(MyGUI::Widget* key, int index); + void unassign(ItemWidget* key, int index); }; class QuickKeysMenuAssign : public WindowModal { public: QuickKeysMenuAssign(QuickKeysMenu* parent); + virtual void exit(); private: MyGUI::TextBox* mLabel; @@ -86,6 +90,7 @@ namespace MWGui MagicSelectionDialog(QuickKeysMenu* parent); virtual void open(); + virtual void exit(); private: MyGUI::Button* mCancelButton; diff --git a/apps/openmw/mwgui/race.cpp b/apps/openmw/mwgui/race.cpp index 499c1e191..2bfc4f141 100644 --- a/apps/openmw/mwgui/race.cpp +++ b/apps/openmw/mwgui/race.cpp @@ -32,7 +32,6 @@ namespace MWGui , mHairIndex(0) , mCurrentAngle(0) , mPreviewDirty(true) - , mPreview(NULL) { // Centre dialog center(); @@ -42,12 +41,11 @@ namespace MWGui getWidget(mHeadRotate, "HeadRotate"); - // Mouse wheel step is hardcoded to 50 in MyGUI 3.2 ("FIXME"). - // Give other steps the same value to accomodate. mHeadRotate->setScrollRange(1000); mHeadRotate->setScrollPosition(500); mHeadRotate->setScrollViewPage(50); mHeadRotate->setScrollPage(50); + mHeadRotate->setScrollWheelPage(50); mHeadRotate->eventScrollChangePosition += MyGUI::newDelegate(this, &RaceDialog::onHeadRotate); // Set up next/previous buttons @@ -115,7 +113,14 @@ namespace MWGui updateSkills(); updateSpellPowers(); - mPreview = new MWRender::RaceSelectionPreview(); + mPreview.reset(NULL); + + mPreviewImage->setImageTexture(""); + + const std::string textureName = "CharacterHeadPreview"; + MyGUI::RenderManager::getInstance().destroyTexture(MyGUI::RenderManager::getInstance().getTexture(textureName)); + + mPreview.reset(new MWRender::RaceSelectionPreview()); mPreview->setup(); mPreview->update (0); @@ -129,7 +134,7 @@ namespace MWGui index = proto.mHair.substr(proto.mHair.size() - 2, 2); mHairIndex = boost::lexical_cast(index) - 1; - mPreviewImage->setImageTexture ("CharacterHeadPreview"); + mPreviewImage->setImageTexture (textureName); mPreviewDirty = true; } @@ -157,8 +162,7 @@ namespace MWGui void RaceDialog::close() { - delete mPreview; - mPreview = 0; + mPreview.reset(NULL); } // widget controls @@ -306,6 +310,10 @@ namespace MWGui void RaceDialog::doRenderUpdate() { + if (!mPreview.get()) + return; + + mPreview->onFrame(); if (mPreviewDirty) { mPreview->render(); @@ -384,7 +392,6 @@ namespace MWGui if (mCurrentRaceId.empty()) return; - Widgets::MWSpellPtr spellPowerWidget; const int lineHeight = 18; MyGUI::IntCoord coord(0, 0, mSpellPowerList->getWidth(), 18); @@ -396,7 +403,7 @@ namespace MWGui for (int i = 0; it != end; ++it) { const std::string &spellpower = *it; - spellPowerWidget = mSpellPowerList->createWidget("MW_StatName", coord, MyGUI::Align::Default, std::string("SpellPower") + boost::lexical_cast(i)); + Widgets::MWSpellPtr spellPowerWidget = mSpellPowerList->createWidget("MW_StatName", coord, MyGUI::Align::Default, std::string("SpellPower") + boost::lexical_cast(i)); spellPowerWidget->setSpellId(spellpower); spellPowerWidget->setUserString("ToolTipType", "Spell"); spellPowerWidget->setUserString("Spell", spellpower); diff --git a/apps/openmw/mwgui/race.hpp b/apps/openmw/mwgui/race.hpp index 340dcfa27..454c1d0b6 100644 --- a/apps/openmw/mwgui/race.hpp +++ b/apps/openmw/mwgui/race.hpp @@ -52,6 +52,11 @@ namespace MWGui */ EventHandle_Void eventBack; + /** Event : Dialog finished, OK button clicked.\n + signature : void method()\n + */ + EventHandle_WindowBase eventDone; + void doRenderUpdate(); protected: @@ -100,7 +105,7 @@ namespace MWGui float mCurrentAngle; - MWRender::RaceSelectionPreview* mPreview; + std::auto_ptr mPreview; bool mPreviewDirty; }; diff --git a/apps/openmw/mwgui/recharge.cpp b/apps/openmw/mwgui/recharge.cpp index 5c4f3eb5a..c45c2566e 100644 --- a/apps/openmw/mwgui/recharge.cpp +++ b/apps/openmw/mwgui/recharge.cpp @@ -14,6 +14,7 @@ #include "../mwmechanics/npcstats.hpp" #include "widgets.hpp" +#include "itemwidget.hpp" namespace MWGui { @@ -38,14 +39,14 @@ void Recharge::open() center(); } +void Recharge::exit() +{ + MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Recharge); +} + void Recharge::start (const MWWorld::Ptr &item) { - std::string path = std::string("icons\\"); - path += item.getClass().getInventoryIcon(item); - int pos = path.rfind("."); - path.erase(pos); - path.append(".dds"); - mGemIcon->setImageTexture (path); + mGemIcon->setItem(item); mGemIcon->setUserString("ToolTipType", "ItemPtr"); mGemIcon->setUserData(item); @@ -103,14 +104,9 @@ void Recharge::updateView() text->setNeedMouseFocus(false); currentY += 19; - MyGUI::ImageBox* icon = mView->createWidget ( - "ImageBox", MyGUI::IntCoord(16, currentY, 32, 32), MyGUI::Align::Default); - std::string path = std::string("icons\\"); - path += iter->getClass().getInventoryIcon(*iter); - int pos = path.rfind("."); - path.erase(pos); - path.append(".dds"); - icon->setImageTexture (path); + ItemWidget* icon = mView->createWidget ( + "MW_ItemIconSmall", MyGUI::IntCoord(16, currentY, 32, 32), MyGUI::Align::Default); + icon->setItem(*iter); icon->setUserString("ToolTipType", "ItemPtr"); icon->setUserData(*iter); icon->eventMouseButtonClick += MyGUI::newDelegate(this, &Recharge::onItemClicked); @@ -123,12 +119,16 @@ void Recharge::updateView() currentY += 32 + 4; } + + // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the scrollbar is hidden + mView->setVisibleVScroll(false); mView->setCanvasSize (MyGUI::IntSize(mView->getWidth(), std::max(mView->getHeight(), currentY))); + mView->setVisibleVScroll(true); } void Recharge::onCancel(MyGUI::Widget *sender) { - MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Recharge); + exit(); } void Recharge::onItemClicked(MyGUI::Widget *sender) @@ -169,6 +169,8 @@ void Recharge::onItemClicked(MyGUI::Widget *sender) item.getCellRef().setEnchantmentCharge( std::min(item.getCellRef().getEnchantmentCharge() + restored, static_cast(enchantment->mData.mCharge))); + player.getClass().getContainerStore(player).restack(item); + player.getClass().skillUsageSucceeded (player, ESM::Skill::Enchant, 0); } @@ -179,6 +181,10 @@ void Recharge::onItemClicked(MyGUI::Widget *sender) std::string message = MWBase::Environment::get().getWorld()->getStore().get().find("sNotifyMessage51")->getString(); message = boost::str(boost::format(message) % gem.getClass().getName(gem)); MWBase::Environment::get().getWindowManager()->messageBox(message); + + // special case: readd Azura's Star + if (Misc::StringUtils::ciEqual(gem.get()->mBase->mId, "Misc_SoulGem_Azura")) + player.getClass().getContainerStore(player).add("Misc_SoulGem_Azura", 1, player); } updateView(); diff --git a/apps/openmw/mwgui/recharge.hpp b/apps/openmw/mwgui/recharge.hpp index 2ffc5e10f..17d700649 100644 --- a/apps/openmw/mwgui/recharge.hpp +++ b/apps/openmw/mwgui/recharge.hpp @@ -8,6 +8,8 @@ namespace MWGui { +class ItemWidget; + class Recharge : public WindowBase { public: @@ -15,6 +17,8 @@ public: virtual void open(); + virtual void exit(); + void start (const MWWorld::Ptr& gem); protected: @@ -23,7 +27,7 @@ protected: MyGUI::Widget* mGemBox; - MyGUI::ImageBox* mGemIcon; + ItemWidget* mGemIcon; MyGUI::TextBox* mChargeLabel; diff --git a/apps/openmw/mwgui/referenceinterface.hpp b/apps/openmw/mwgui/referenceinterface.hpp index df53a42b7..0ba180de8 100644 --- a/apps/openmw/mwgui/referenceinterface.hpp +++ b/apps/openmw/mwgui/referenceinterface.hpp @@ -17,7 +17,7 @@ namespace MWGui void checkReferenceAvailable(); ///< closes the window, if the MW-reference has become unavailable - void resetReference() { mPtr = MWWorld::Ptr(); mCurrentPlayerCell = NULL; } + virtual void resetReference() { mPtr = MWWorld::Ptr(); mCurrentPlayerCell = NULL; } protected: virtual void onReferenceUnavailable() = 0; ///< called when reference has become unavailable diff --git a/apps/openmw/mwgui/repair.cpp b/apps/openmw/mwgui/repair.cpp index 1ae02599e..019f341b5 100644 --- a/apps/openmw/mwgui/repair.cpp +++ b/apps/openmw/mwgui/repair.cpp @@ -13,6 +13,8 @@ #include "widgets.hpp" +#include "itemwidget.hpp" + namespace MWGui { @@ -35,16 +37,16 @@ void Repair::open() center(); } +void Repair::exit() +{ + MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Repair); +} + void Repair::startRepairItem(const MWWorld::Ptr &item) { mRepair.setTool(item); - std::string path = std::string("icons\\"); - path += item.getClass().getInventoryIcon(item); - int pos = path.rfind("."); - path.erase(pos); - path.append(".dds"); - mToolIcon->setImageTexture (path); + mToolIcon->setItem(item); mToolIcon->setUserString("ToolTipType", "ItemPtr"); mToolIcon->setUserData(item); @@ -108,14 +110,9 @@ void Repair::updateRepairView() text->setNeedMouseFocus(false); currentY += 19; - MyGUI::ImageBox* icon = mRepairView->createWidget ( - "ImageBox", MyGUI::IntCoord(16, currentY, 32, 32), MyGUI::Align::Default); - std::string path = std::string("icons\\"); - path += iter->getClass().getInventoryIcon(*iter); - int pos = path.rfind("."); - path.erase(pos); - path.append(".dds"); - icon->setImageTexture (path); + ItemWidget* icon = mRepairView->createWidget ( + "MW_ItemIconSmall", MyGUI::IntCoord(16, currentY, 32, 32), MyGUI::Align::Default); + icon->setItem(*iter); icon->setUserString("ToolTipType", "ItemPtr"); icon->setUserData(*iter); icon->eventMouseButtonClick += MyGUI::newDelegate(this, &Repair::onRepairItem); @@ -129,12 +126,15 @@ void Repair::updateRepairView() currentY += 32 + 4; } } + // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the scrollbar is hidden + mRepairView->setVisibleVScroll(false); mRepairView->setCanvasSize (MyGUI::IntSize(mRepairView->getWidth(), std::max(mRepairView->getHeight(), currentY))); + mRepairView->setVisibleVScroll(true); } void Repair::onCancel(MyGUI::Widget *sender) { - MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Repair); + exit(); } void Repair::onRepairItem(MyGUI::Widget *sender) diff --git a/apps/openmw/mwgui/repair.hpp b/apps/openmw/mwgui/repair.hpp index d0f5c54c4..439ee1169 100644 --- a/apps/openmw/mwgui/repair.hpp +++ b/apps/openmw/mwgui/repair.hpp @@ -8,6 +8,8 @@ namespace MWGui { +class ItemWidget; + class Repair : public WindowBase { public: @@ -15,6 +17,8 @@ public: virtual void open(); + virtual void exit(); + void startRepairItem (const MWWorld::Ptr& item); protected: @@ -23,7 +27,7 @@ protected: MyGUI::Widget* mToolBox; - MyGUI::ImageBox* mToolIcon; + ItemWidget* mToolIcon; MyGUI::TextBox* mUsesLabel; MyGUI::TextBox* mQualityLabel; diff --git a/apps/openmw/mwgui/review.cpp b/apps/openmw/mwgui/review.cpp index e27e40ae6..d1f8a76a6 100644 --- a/apps/openmw/mwgui/review.cpp +++ b/apps/openmw/mwgui/review.cpp @@ -27,22 +27,22 @@ namespace MWGui getWidget(mNameWidget, "NameText"); getWidget(button, "NameButton"); adjustButtonSize(button); - button->eventMouseButtonClick += MyGUI::newDelegate(this, &ReviewDialog::onNameClicked);; + button->eventMouseButtonClick += MyGUI::newDelegate(this, &ReviewDialog::onNameClicked); getWidget(mRaceWidget, "RaceText"); getWidget(button, "RaceButton"); adjustButtonSize(button); - button->eventMouseButtonClick += MyGUI::newDelegate(this, &ReviewDialog::onRaceClicked);; + button->eventMouseButtonClick += MyGUI::newDelegate(this, &ReviewDialog::onRaceClicked); getWidget(mClassWidget, "ClassText"); getWidget(button, "ClassButton"); adjustButtonSize(button); - button->eventMouseButtonClick += MyGUI::newDelegate(this, &ReviewDialog::onClassClicked);; + button->eventMouseButtonClick += MyGUI::newDelegate(this, &ReviewDialog::onClassClicked); getWidget(mBirthSignWidget, "SignText"); getWidget(button, "SignButton"); adjustButtonSize(button); - button->eventMouseButtonClick += MyGUI::newDelegate(this, &ReviewDialog::onBirthSignClicked);; + button->eventMouseButtonClick += MyGUI::newDelegate(this, &ReviewDialog::onBirthSignClicked); // Setup dynamic stats getWidget(mHealth, "Health"); @@ -320,7 +320,10 @@ namespace MWGui if (!mMiscSkills.empty()) addSkills(mMiscSkills, "sSkillClassMisc", "Misc Skills", coord1, coord2); + // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the scrollbar is hidden + mSkillView->setVisibleVScroll(false); mSkillView->setCanvasSize (mSkillView->getWidth(), std::max(mSkillView->getHeight(), coord1.top)); + mSkillView->setVisibleVScroll(true); } // widget controls diff --git a/apps/openmw/mwgui/review.hpp b/apps/openmw/mwgui/review.hpp index 5d0767d21..01b106d90 100644 --- a/apps/openmw/mwgui/review.hpp +++ b/apps/openmw/mwgui/review.hpp @@ -54,6 +54,11 @@ namespace MWGui */ EventHandle_Void eventBack; + /** Event : Dialog finished, OK button clicked.\n + signature : void method()\n + */ + EventHandle_WindowBase eventDone; + EventHandle_Int eventActivateDialog; protected: diff --git a/apps/openmw/mwgui/savegamedialog.cpp b/apps/openmw/mwgui/savegamedialog.cpp index 6048e49b4..66c7a9238 100644 --- a/apps/openmw/mwgui/savegamedialog.cpp +++ b/apps/openmw/mwgui/savegamedialog.cpp @@ -30,15 +30,18 @@ namespace MWGui getWidget(mInfoText, "InfoText"); getWidget(mOkButton, "OkButton"); getWidget(mCancelButton, "CancelButton"); + getWidget(mDeleteButton, "DeleteButton"); getWidget(mSaveList, "SaveList"); getWidget(mSaveNameEdit, "SaveNameEdit"); getWidget(mSpacer, "Spacer"); mOkButton->eventMouseButtonClick += MyGUI::newDelegate(this, &SaveGameDialog::onOkButtonClicked); mCancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &SaveGameDialog::onCancelButtonClicked); - mCharacterSelection->eventComboChangePosition += MyGUI::newDelegate(this, &SaveGameDialog::onCharacterSelected); + mDeleteButton->eventMouseButtonClick += MyGUI::newDelegate(this, &SaveGameDialog::onDeleteButtonClicked); + mCharacterSelection->eventComboAccept += MyGUI::newDelegate(this, &SaveGameDialog::onCharacterSelected); mSaveList->eventListChangePosition += MyGUI::newDelegate(this, &SaveGameDialog::onSlotSelected); mSaveList->eventListMouseItemActivate += MyGUI::newDelegate(this, &SaveGameDialog::onSlotMouseClick); mSaveList->eventListSelectAccept += MyGUI::newDelegate(this, &SaveGameDialog::onSlotActivated); + mSaveList->eventKeyButtonPressed += MyGUI::newDelegate(this, &SaveGameDialog::onKeyButtonPressed); mSaveNameEdit->eventEditSelectAccept += MyGUI::newDelegate(this, &SaveGameDialog::onEditSelectAccept); mSaveNameEdit->eventEditTextChange += MyGUI::newDelegate(this, &SaveGameDialog::onSaveNameChanged); } @@ -54,13 +57,16 @@ namespace MWGui onSlotSelected(sender, pos); if (pos != MyGUI::ITEM_NONE && MyGUI::InputManager::getInstance().isShiftPressed()) - { - ConfirmationDialog* dialog = MWBase::Environment::get().getWindowManager()->getConfirmationDialog(); - dialog->open("#{sMessage3}"); - dialog->eventOkClicked.clear(); - dialog->eventOkClicked += MyGUI::newDelegate(this, &SaveGameDialog::onDeleteSlotConfirmed); - dialog->eventCancelClicked.clear(); - } + confirmDeleteSave(); + } + + void SaveGameDialog::confirmDeleteSave() + { + ConfirmationDialog* dialog = MWBase::Environment::get().getWindowManager()->getConfirmationDialog(); + dialog->open("#{sMessage3}"); + dialog->eventOkClicked.clear(); + dialog->eventOkClicked += MyGUI::newDelegate(this, &SaveGameDialog::onDeleteSlotConfirmed); + dialog->eventCancelClicked.clear(); } void SaveGameDialog::onDeleteSlotConfirmed() @@ -107,6 +113,7 @@ namespace MWGui mCurrentCharacter = NULL; mCurrentSlot = NULL; mSaveList->removeAllItems(); + onSlotSelected(mSaveList, MyGUI::ITEM_NONE); MWBase::StateManager* mgr = MWBase::Environment::get().getStateManager(); if (mgr->characterBegin() == mgr->characterEnd()) @@ -117,7 +124,7 @@ namespace MWGui std::string directory = Misc::StringUtils::lowerCase (Settings::Manager::getString ("character", "Saves")); - int selectedIndex = MyGUI::ITEM_NONE; + size_t selectedIndex = MyGUI::ITEM_NONE; for (MWBase::StateManager::CharacterIterator it = mgr->characterBegin(); it != mgr->characterEnd(); ++it) { @@ -134,9 +141,12 @@ namespace MWGui else { // Find the localised name for this class from the store - const ESM::Class* class_ = MWBase::Environment::get().getWorld()->getStore().get().find( + const ESM::Class* class_ = MWBase::Environment::get().getWorld()->getStore().get().search( it->getSignature().mPlayerClassId); - className = class_->mName; + if (class_) + className = class_->mName; + else + className = "?"; // From an older savegame format that did not support custom classes properly. } title << " (Level " << it->getSignature().mPlayerLevel << " " << className << ")"; @@ -154,11 +164,18 @@ namespace MWGui } mCharacterSelection->setIndexSelected(selectedIndex); + if (selectedIndex == MyGUI::ITEM_NONE) + mCharacterSelection->setCaption("Select Character ..."); fillSaveList(); } + void SaveGameDialog::exit() + { + setVisible(false); + } + void SaveGameDialog::setLoadOrSave(bool load) { mSaving = !load; @@ -167,6 +184,9 @@ namespace MWGui mCharacterSelection->setVisible(load); mSpacer->setUserString("Hidden", load ? "false" : "true"); + mDeleteButton->setUserString("Hidden", load ? "false" : "true"); + mDeleteButton->setVisible(load); + if (!load) { mCurrentCharacter = MWBase::Environment::get().getStateManager()->getCurrentCharacter (false); @@ -177,7 +197,13 @@ namespace MWGui void SaveGameDialog::onCancelButtonClicked(MyGUI::Widget *sender) { - setVisible(false); + exit(); + } + + void SaveGameDialog::onDeleteButtonClicked(MyGUI::Widget *sender) + { + if (mCurrentSlot) + confirmDeleteSave(); } void SaveGameDialog::onConfirmationGiven() @@ -187,6 +213,7 @@ namespace MWGui void SaveGameDialog::accept(bool reallySure) { + // Remove for MyGUI 3.2.2 MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(NULL); if (mSaving) @@ -217,13 +244,17 @@ namespace MWGui } else { - if (mCurrentCharacter && mCurrentSlot) - { - MWBase::Environment::get().getStateManager()->loadGame (mCurrentCharacter, mCurrentSlot); - } + assert (mCurrentCharacter && mCurrentSlot); + MWBase::Environment::get().getStateManager()->loadGame (mCurrentCharacter, mCurrentSlot); } } + void SaveGameDialog::onKeyButtonPressed(MyGUI::Widget* _sender, MyGUI::KeyCode key, MyGUI::Char character) + { + if (key == MyGUI::KeyCode::Delete && mCurrentSlot) + confirmDeleteSave(); + } + void SaveGameDialog::onOkButtonClicked(MyGUI::Widget *sender) { accept(); @@ -270,6 +301,9 @@ namespace MWGui void SaveGameDialog::onSlotSelected(MyGUI::ListBox *sender, size_t pos) { + mOkButton->setEnabled(pos != MyGUI::ITEM_NONE || mSaving); + mDeleteButton->setEnabled(pos != MyGUI::ITEM_NONE); + if (pos == MyGUI::ITEM_NONE) { mCurrentSlot = NULL; @@ -288,7 +322,8 @@ namespace MWGui if (i == pos) mCurrentSlot = &*it; } - assert(mCurrentSlot && "Can't find selected slot"); + if (!mCurrentSlot) + throw std::runtime_error("Can't find selected slot"); std::stringstream text; time_t time = mCurrentSlot->mTimeStamp; diff --git a/apps/openmw/mwgui/savegamedialog.hpp b/apps/openmw/mwgui/savegamedialog.hpp index 42c29f4bc..11470a20f 100644 --- a/apps/openmw/mwgui/savegamedialog.hpp +++ b/apps/openmw/mwgui/savegamedialog.hpp @@ -19,11 +19,17 @@ namespace MWGui virtual void open(); + virtual void exit(); + void setLoadOrSave(bool load); private: + void confirmDeleteSave(); + + void onKeyButtonPressed(MyGUI::Widget* _sender, MyGUI::KeyCode key, MyGUI::Char character); void onCancelButtonClicked (MyGUI::Widget* sender); void onOkButtonClicked (MyGUI::Widget* sender); + void onDeleteButtonClicked (MyGUI::Widget* sender); void onCharacterSelected (MyGUI::ComboBox* sender, size_t pos); // Slot selected (mouse click or arrow keys) void onSlotSelected (MyGUI::ListBox* sender, size_t pos); @@ -49,6 +55,7 @@ namespace MWGui MyGUI::EditBox* mInfoText; MyGUI::Button* mOkButton; MyGUI::Button* mCancelButton; + MyGUI::Button* mDeleteButton; MyGUI::ListBox* mSaveList; MyGUI::EditBox* mSaveNameEdit; MyGUI::Widget* mSpacer; diff --git a/apps/openmw/mwgui/screenfader.cpp b/apps/openmw/mwgui/screenfader.cpp new file mode 100644 index 000000000..a0421ec28 --- /dev/null +++ b/apps/openmw/mwgui/screenfader.cpp @@ -0,0 +1,176 @@ +#include "screenfader.hpp" + +namespace MWGui +{ + + FadeOp::FadeOp(ScreenFader * fader, float time, float targetAlpha) + : mFader(fader), + mRemainingTime(time), + mTargetTime(time), + mTargetAlpha(targetAlpha), + mStartAlpha(0.f), + mRunning(false) + { + } + + bool FadeOp::isRunning() + { + return mRunning; + } + + void FadeOp::start() + { + if (mRunning) + return; + + mRemainingTime = mTargetTime; + mStartAlpha = mFader->getCurrentAlpha(); + mRunning = true; + } + + void FadeOp::update(float dt) + { + if (!mRunning) + return; + + if (mRemainingTime <= 0 || mStartAlpha == mTargetAlpha) + { + finish(); + return; + } + + float currentAlpha = mFader->getCurrentAlpha(); + if (mStartAlpha > mTargetAlpha) + { + currentAlpha -= dt/mTargetTime * (mStartAlpha-mTargetAlpha); + if (currentAlpha < mTargetAlpha) + currentAlpha = mTargetAlpha; + } + else + { + currentAlpha += dt/mTargetTime * (mTargetAlpha-mStartAlpha); + if (currentAlpha > mTargetAlpha) + currentAlpha = mTargetAlpha; + } + + mFader->notifyAlphaChanged(currentAlpha); + + mRemainingTime -= dt; + } + + void FadeOp::finish() + { + mRunning = false; + mFader->notifyOperationFinished(); + } + + ScreenFader::ScreenFader(const std::string & texturePath) + : WindowBase("openmw_screen_fader.layout") + , mCurrentAlpha(0.f) + , mFactor(1.f) + , mRepeat(false) + { + mMainWidget->setSize(MyGUI::RenderManager::getInstance().getViewSize()); + setTexture(texturePath); + setVisible(false); + } + + void ScreenFader::setTexture(const std::string & texturePath) + { + mMainWidget->setProperty("ImageTexture", texturePath); + } + + void ScreenFader::update(float dt) + { + if (!mQueue.empty()) + { + if (!mQueue.front()->isRunning()) + mQueue.front()->start(); + mQueue.front()->update(dt); + } + } + + void ScreenFader::applyAlpha() + { + setVisible(true); + mMainWidget->setAlpha(1.f-((1.f-mCurrentAlpha) * mFactor)); + } + + void ScreenFader::fadeIn(float time) + { + queue(time, 1.f); + } + + void ScreenFader::fadeOut(const float time) + { + queue(time, 0.f); + } + + void ScreenFader::fadeTo(const int percent, const float time) + { + queue(time, percent/100.f); + } + + void ScreenFader::setFactor(float factor) + { + mFactor = factor; + applyAlpha(); + } + + void ScreenFader::setRepeat(bool repeat) + { + mRepeat = repeat; + } + + void ScreenFader::queue(float time, float targetAlpha) + { + if (time < 0.f) + return; + + if (time == 0.f) + { + mCurrentAlpha = targetAlpha; + applyAlpha(); + return; + } + + mQueue.push_back(FadeOp::Ptr(new FadeOp(this, time, targetAlpha))); + } + + bool ScreenFader::isEmpty() + { + return mQueue.empty(); + } + + void ScreenFader::clearQueue() + { + mQueue.clear(); + } + + void ScreenFader::notifyAlphaChanged(float alpha) + { + if (mCurrentAlpha == alpha) + return; + + mCurrentAlpha = alpha; + + if (1.f-((1.f-mCurrentAlpha) * mFactor) == 0.f) + mMainWidget->setVisible(false); + else + applyAlpha(); + } + + void ScreenFader::notifyOperationFinished() + { + FadeOp::Ptr op = mQueue.front(); + mQueue.pop_front(); + + if (mRepeat) + mQueue.push_back(op); + } + + float ScreenFader::getCurrentAlpha() + { + return mCurrentAlpha; + } +} diff --git a/apps/openmw/mwgui/screenfader.hpp b/apps/openmw/mwgui/screenfader.hpp new file mode 100644 index 000000000..402554555 --- /dev/null +++ b/apps/openmw/mwgui/screenfader.hpp @@ -0,0 +1,71 @@ +#ifndef OPENMW_MWGUI_SCREENFADER_H +#define OPENMW_MWGUI_SCREENFADER_H + +#include + +#include + +#include "windowbase.hpp" + +namespace MWGui +{ + class ScreenFader; + + class FadeOp + { + public: + typedef boost::shared_ptr Ptr; + + FadeOp(ScreenFader * fader, float time, float targetAlpha); + + bool isRunning(); + + void start(); + void update(float dt); + void finish(); + + private: + ScreenFader * mFader; + float mRemainingTime; + float mTargetTime; + float mTargetAlpha; + float mStartAlpha; + bool mRunning; + }; + + class ScreenFader : public WindowBase + { + public: + ScreenFader(const std::string & texturePath); + + void setTexture(const std::string & texturePath); + + void update(float dt); + + void fadeIn(const float time); + void fadeOut(const float time); + void fadeTo(const int percent, const float time); + + void setFactor (float factor); + void setRepeat(bool repeat); + + void queue(float time, float targetAlpha); + bool isEmpty(); + void clearQueue(); + + void notifyAlphaChanged(float alpha); + void notifyOperationFinished(); + float getCurrentAlpha(); + + private: + void applyAlpha(); + + float mCurrentAlpha; + float mFactor; + + bool mRepeat; // repeat queued operations without removing them + std::deque mQueue; + }; +} + +#endif diff --git a/apps/openmw/mwgui/scrollwindow.cpp b/apps/openmw/mwgui/scrollwindow.cpp index a084e6453..038d91ca9 100644 --- a/apps/openmw/mwgui/scrollwindow.cpp +++ b/apps/openmw/mwgui/scrollwindow.cpp @@ -13,7 +13,7 @@ namespace { - void adjustButton (MWGui::ImageButton* button) + void adjustButton (Gui::ImageButton* button) { MyGUI::IntSize diff = button->getSize() - button->getRequestedSize(); button->setSize(button->getRequestedSize()); @@ -54,19 +54,30 @@ namespace MWGui MWWorld::LiveCellRef *ref = mScroll.get(); - BookTextParser parser; - MyGUI::IntSize size = parser.parseScroll(ref->mBase->mText, mTextView, 390); + Formatting::BookFormatter formatter; + formatter.markupToWidget(mTextView, ref->mBase->mText, 390, mTextView->getHeight()); + MyGUI::IntSize size = mTextView->getChildAt(0)->getSize(); + // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the scrollbar is hidden + mTextView->setVisibleVScroll(false); if (size.height > mTextView->getSize().height) mTextView->setCanvasSize(MyGUI::IntSize(410, size.height)); else mTextView->setCanvasSize(410, mTextView->getSize().height); + mTextView->setVisibleVScroll(true); mTextView->setViewOffset(MyGUI::IntPoint(0,0)); setTakeButtonShow(true); } + void ScrollWindow::exit() + { + MWBase::Environment::get().getSoundManager()->playSound ("scroll", 1.0, 1.0); + + MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Scroll); + } + void ScrollWindow::setTakeButtonShow(bool show) { mTakeButtonShow = show; @@ -81,9 +92,7 @@ namespace MWGui void ScrollWindow::onCloseButtonClicked (MyGUI::Widget* _sender) { - MWBase::Environment::get().getSoundManager()->playSound ("scroll", 1.0, 1.0); - - MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Scroll); + exit(); } void ScrollWindow::onTakeButtonClicked (MyGUI::Widget* _sender) diff --git a/apps/openmw/mwgui/scrollwindow.hpp b/apps/openmw/mwgui/scrollwindow.hpp index 5feaff7bf..08ce6a905 100644 --- a/apps/openmw/mwgui/scrollwindow.hpp +++ b/apps/openmw/mwgui/scrollwindow.hpp @@ -2,7 +2,8 @@ #define MWGUI_SCROLLWINDOW_H #include "windowbase.hpp" -#include "imagebutton.hpp" + +#include #include "../mwworld/ptr.hpp" @@ -14,6 +15,7 @@ namespace MWGui ScrollWindow (); void open (MWWorld::Ptr scroll); + virtual void exit(); void setTakeButtonShow(bool show); void setInventoryAllowed(bool allowed); @@ -22,8 +24,8 @@ namespace MWGui void onTakeButtonClicked (MyGUI::Widget* _sender); private: - MWGui::ImageButton* mCloseButton; - MWGui::ImageButton* mTakeButton; + Gui::ImageButton* mCloseButton; + Gui::ImageButton* mTakeButton; MyGUI::ScrollView* mTextView; MWWorld::Ptr mScroll; diff --git a/apps/openmw/mwgui/settingswindow.cpp b/apps/openmw/mwgui/settingswindow.cpp index 78adecd3e..948423a35 100644 --- a/apps/openmw/mwgui/settingswindow.cpp +++ b/apps/openmw/mwgui/settingswindow.cpp @@ -73,17 +73,6 @@ namespace return (Ogre::Root::getSingleton ().getRenderSystem ()->getName ().find("OpenGL") != std::string::npos) ? "glsl" : "hlsl"; } - bool cgAvailable () - { - Ogre::Root::PluginInstanceList list = Ogre::Root::getSingleton ().getInstalledPlugins (); - for (Ogre::Root::PluginInstanceList::const_iterator it = list.begin(); it != list.end(); ++it) - { - if ((*it)->getName() == "Cg Program Manager") - return true; - } - return false; - } - const char* checkButtonType = "CheckButton"; const char* sliderType = "Slider"; @@ -147,6 +136,7 @@ namespace MWGui float min,max; getSettingMinMax(scroll, min, max); float value = Settings::Manager::getFloat(getSettingName(current), getSettingCategory(current)); + value = std::max(min, std::min(value, max)); value = (value-min)/(max-min); scroll->setScrollPosition( value * (scroll->getScrollRange()-1)); @@ -168,6 +158,8 @@ namespace MWGui { configureWidgets(mMainWidget); + setTitle("#{sOptions}"); + getWidget(mOkButton, "OkButton"); getWidget(mResolutionList, "ResolutionList"); getWidget(mFullscreenButton, "FullscreenButton"); @@ -185,6 +177,9 @@ namespace MWGui getWidget(mControlsBox, "ControlsBox"); getWidget(mResetControlsButton, "ResetControlsButton"); getWidget(mRefractionButton, "RefractionButton"); + getWidget(mDifficultySlider, "DifficultySlider"); + + mMainWidget->castType()->eventWindowChangeCoord += MyGUI::newDelegate(this, &SettingsWindow::onWindowResize); mOkButton->eventMouseButtonClick += MyGUI::newDelegate(this, &SettingsWindow::onOkButtonClicked); mShaderModeButton->eventMouseButtonClick += MyGUI::newDelegate(this, &SettingsWindow::onShaderModeToggled); @@ -238,11 +233,15 @@ namespace MWGui MyGUI::TextBox* fovText; getWidget(fovText, "FovText"); fovText->setCaption("Field of View (" + boost::lexical_cast(int(Settings::Manager::getInt("field of view", "General"))) + ")"); + + MyGUI::TextBox* diffText; + getWidget(diffText, "DifficultyText"); + diffText->setCaptionWithReplacing("#{sDifficulty} (" + boost::lexical_cast(int(Settings::Manager::getInt("difficulty", "Game"))) + ")"); } void SettingsWindow::onOkButtonClicked(MyGUI::Widget* _sender) { - MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Settings); + exit(); } void SettingsWindow::onResolutionSelected(MyGUI::ListBox* _sender, size_t index) @@ -297,15 +296,6 @@ namespace MWGui newState = true; } - if (_sender == mVSyncButton) - { - // Ogre::Window::setVSyncEnabled is bugged in 1.8 -#if OGRE_VERSION < (1 << 16 | 9 << 8 | 0) - MWBase::Environment::get().getWindowManager()-> - messageBox("VSync will be applied after a restart", std::vector()); -#endif - } - if (_sender == mShadersButton) { if (newState == false) @@ -333,6 +323,15 @@ namespace MWGui if (_sender == mFullscreenButton) { // check if this resolution is supported in fullscreen + if (mResolutionList->getIndexSelected() != MyGUI::ITEM_NONE) + { + std::string resStr = mResolutionList->getItemNameAt(mResolutionList->getIndexSelected()); + int resX, resY; + parseResolution (resX, resY, resStr); + Settings::Manager::setInt("resolution x", "Video", resX); + Settings::Manager::setInt("resolution y", "Video", resY); + } + bool supported = false; for (unsigned int i=0; igetItemCount(); ++i) { @@ -365,15 +364,9 @@ namespace MWGui void SettingsWindow::onShaderModeToggled(MyGUI::Widget* _sender) { - std::string val = static_cast(_sender)->getCaption(); - if (val == "cg") - { - val = hlslGlsl(); - } - else if (cgAvailable ()) - val = "cg"; + std::string val = hlslGlsl(); - static_cast(_sender)->setCaption(val); + _sender->castType()->setCaption(val); Settings::Manager::setString("shader mode", "General", val); @@ -413,6 +406,12 @@ namespace MWGui getWidget(fovText, "FovText"); fovText->setCaption("Field of View (" + boost::lexical_cast(int(value)) + ")"); } + if (scroller == mDifficultySlider) + { + MyGUI::TextBox* diffText; + getWidget(diffText, "DifficultyText"); + diffText->setCaptionWithReplacing("#{sDifficulty} (" + boost::lexical_cast(int(value)) + ")"); + } } else { @@ -467,14 +466,17 @@ namespace MWGui curH += h; } + // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the scrollbar is hidden + mControlsBox->setVisibleVScroll(false); mControlsBox->setCanvasSize (mControlsBox->getWidth(), std::max(curH, mControlsBox->getHeight())); + mControlsBox->setVisibleVScroll(true); } void SettingsWindow::onRebindAction(MyGUI::Widget* _sender) { int actionId = *_sender->getUserData(); - static_cast(_sender)->setCaptionWithReplacing("#{sNone}"); + _sender->castType()->setCaptionWithReplacing("#{sNone}"); MWBase::Environment::get().getWindowManager ()->staticMessageBox ("#{sControlsMenu3}"); MWBase::Environment::get().getWindowManager ()->disallowMouse(); @@ -510,4 +512,14 @@ namespace MWGui { updateControlsBox (); } + + void SettingsWindow::exit() + { + MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Settings); + } + + void SettingsWindow::onWindowResize(MyGUI::Window *_sender) + { + updateControlsBox(); + } } diff --git a/apps/openmw/mwgui/settingswindow.hpp b/apps/openmw/mwgui/settingswindow.hpp index 7a6c1a5ed..2619943f9 100644 --- a/apps/openmw/mwgui/settingswindow.hpp +++ b/apps/openmw/mwgui/settingswindow.hpp @@ -17,6 +17,8 @@ namespace MWGui virtual void open(); + virtual void exit(); + void updateControlsBox(); protected: @@ -28,6 +30,7 @@ namespace MWGui MyGUI::Button* mVSyncButton; MyGUI::Button* mFPSButton; MyGUI::ScrollBar* mFOVSlider; + MyGUI::ScrollBar* mDifficultySlider; MyGUI::ScrollBar* mAnisotropySlider; MyGUI::ComboBox* mTextureFilteringButton; MyGUI::TextBox* mAnisotropyLabel; @@ -60,6 +63,8 @@ namespace MWGui void onResetDefaultBindings(MyGUI::Widget* _sender); void onResetDefaultBindingsAccept (); + void onWindowResize(MyGUI::Window* _sender); + void apply(); void configureWidgets(MyGUI::Widget* widget); diff --git a/apps/openmw/mwgui/sortfilteritemmodel.cpp b/apps/openmw/mwgui/sortfilteritemmodel.cpp index b8dcbcbbb..93e5432ca 100644 --- a/apps/openmw/mwgui/sortfilteritemmodel.cpp +++ b/apps/openmw/mwgui/sortfilteritemmodel.cpp @@ -14,6 +14,7 @@ #include #include "../mwworld/class.hpp" +#include "../mwworld/nullaction.hpp" namespace { @@ -126,6 +127,10 @@ namespace MWGui && !base.get()->mBase->mData.mIsScroll) return false; + if ((mFilter & Filter_OnlyUsableItems) && typeid(*base.getClass().use(base)) == typeid(MWWorld::NullAction) + && base.getClass().getScript(base).empty()) + return false; + return true; } diff --git a/apps/openmw/mwgui/sortfilteritemmodel.hpp b/apps/openmw/mwgui/sortfilteritemmodel.hpp index c7feaa3b9..4af35e7a8 100644 --- a/apps/openmw/mwgui/sortfilteritemmodel.hpp +++ b/apps/openmw/mwgui/sortfilteritemmodel.hpp @@ -36,6 +36,7 @@ namespace MWGui static const int Filter_OnlyEnchanted = (1<<1); static const int Filter_OnlyEnchantable = (1<<2); static const int Filter_OnlyChargedSoulstones = (1<<3); + static const int Filter_OnlyUsableItems = (1<<4); // Only items with a Use action private: diff --git a/apps/openmw/mwgui/spellbuyingwindow.cpp b/apps/openmw/mwgui/spellbuyingwindow.cpp index 99b85c13c..38b1bce7b 100644 --- a/apps/openmw/mwgui/spellbuyingwindow.cpp +++ b/apps/openmw/mwgui/spellbuyingwindow.cpp @@ -33,6 +33,11 @@ namespace MWGui mCancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &SpellBuyingWindow::onCancelButtonClicked); } + void SpellBuyingWindow::exit() + { + MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_SpellBuying); + } + void SpellBuyingWindow::addSpell(const std::string& spellId) { const MWWorld::ESMStore &store = @@ -45,16 +50,17 @@ namespace MWGui MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); int playerGold = player.getClass().getContainerStore(player).count(MWWorld::ContainerStore::sGoldId); + // TODO: refactor to use MyGUI::ListBox + MyGUI::Button* toAdd = mSpellsView->createWidget( - "SandTextButton", + price <= playerGold ? "SandTextButton" : "SandTextButtonDisabled", // can't use setEnabled since that removes tooltip 0, mCurrentY, 200, sLineHeight, MyGUI::Align::Default ); - toAdd->setEnabled(price<=playerGold); mCurrentY += sLineHeight; @@ -93,6 +99,15 @@ namespace MWGui if (spell->mData.mType!=ESM::Spell::ST_Spell) continue; // don't try to sell diseases, curses or powers + if (actor.getClass().isNpc()) + { + const ESM::Race* race = + MWBase::Environment::get().getWorld()->getStore().get().find( + actor.get()->mBase->mRace); + if (race->mPowers.exists(spell->mId)) + continue; + } + if (playerHasSpell(iter->first)) continue; @@ -101,7 +116,10 @@ namespace MWGui updateLabels(); + // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the scrollbar is hidden + mSpellsView->setVisibleVScroll(false); mSpellsView->setCanvasSize (MyGUI::IntSize(mSpellsView->getWidth(), std::max(mSpellsView->getHeight(), mCurrentY))); + mSpellsView->setVisibleVScroll(true); } bool SpellBuyingWindow::playerHasSpell(const std::string &id) @@ -121,10 +139,18 @@ namespace MWGui int price = *_sender->getUserData(); MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); + if (price > player.getClass().getContainerStore(player).count(MWWorld::ContainerStore::sGoldId)) + return; + MWMechanics::CreatureStats& stats = player.getClass().getCreatureStats(player); MWMechanics::Spells& spells = stats.getSpells(); spells.add (mSpellsWidgetMap.find(_sender)->second); player.getClass().getContainerStore(player).remove(MWWorld::ContainerStore::sGoldId, price, player); + + // add gold to NPC trading gold pool + MWMechanics::CreatureStats& npcStats = mPtr.getClass().getCreatureStats(mPtr); + npcStats.setGoldPool(npcStats.getGoldPool() + price); + startSpellBuying(mPtr); MWBase::Environment::get().getSoundManager()->playSound ("Item Gold Up", 1.0, 1.0); @@ -132,7 +158,7 @@ namespace MWGui void SpellBuyingWindow::onCancelButtonClicked(MyGUI::Widget* _sender) { - MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_SpellBuying); + exit(); } void SpellBuyingWindow::updateLabels() diff --git a/apps/openmw/mwgui/spellbuyingwindow.hpp b/apps/openmw/mwgui/spellbuyingwindow.hpp index f7ea54c89..2a6dcfdcc 100644 --- a/apps/openmw/mwgui/spellbuyingwindow.hpp +++ b/apps/openmw/mwgui/spellbuyingwindow.hpp @@ -25,6 +25,8 @@ namespace MWGui void startSpellBuying(const MWWorld::Ptr& actor); + virtual void exit(); + protected: MyGUI::Button* mCancelButton; MyGUI::TextBox* mPlayerGold; diff --git a/apps/openmw/mwgui/spellcreationdialog.cpp b/apps/openmw/mwgui/spellcreationdialog.cpp index 1ec4bd6da..00cab6c45 100644 --- a/apps/openmw/mwgui/spellcreationdialog.cpp +++ b/apps/openmw/mwgui/spellcreationdialog.cpp @@ -2,6 +2,8 @@ #include +#include + #include "../mwbase/windowmanager.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwbase/mechanicsmanager.hpp" @@ -37,6 +39,7 @@ namespace MWGui EditEffectDialog::EditEffectDialog() : WindowModal("openmw_edit_effect.layout") , mEditing(false) + , mMagicEffect(NULL) { getWidget(mCancelButton, "CancelButton"); getWidget(mOkButton, "OkButton"); @@ -75,6 +78,15 @@ namespace MWGui center(); } + void EditEffectDialog::exit() + { + setVisible(false); + if(mEditing) + eventEffectModified(mOldEffect); + else + eventEffectRemoved(mEffect); + } + void EditEffectDialog::newEffect (const ESM::MagicEffect *effect) { setMagicEffect(effect); @@ -134,13 +146,7 @@ namespace MWGui void EditEffectDialog::setMagicEffect (const ESM::MagicEffect *effect) { - std::string icon = effect->mIcon; - icon[icon.size()-3] = 'd'; - icon[icon.size()-2] = 'd'; - icon[icon.size()-1] = 's'; - icon = "icons\\" + icon; - - mEffectImage->setImageTexture (icon); + mEffectImage->setImageTexture(Misc::ResourceHelpers::correctIconPath(effect->mIcon)); mEffectName->setCaptionWithReplacing("#{"+ESM::MagicEffect::effectIdToString (effect->mIndex)+"}"); @@ -176,7 +182,7 @@ namespace MWGui { mAreaBox->setPosition(mAreaBox->getPosition().left, curY); mAreaBox->setVisible (true); - curY += mAreaBox->getSize().height; + //curY += mAreaBox->getSize().height; } } @@ -222,11 +228,7 @@ namespace MWGui void EditEffectDialog::onCancelButtonClicked (MyGUI::Widget* sender) { - setVisible(false); - if(mEditing) - eventEffectModified(mOldEffect); - else - eventEffectRemoved(mEffect); + exit(); } void EditEffectDialog::setSkill (int skill) @@ -286,7 +288,7 @@ namespace MWGui SpellCreationDialog::SpellCreationDialog() : WindowBase("openmw_spellcreation_dialog.layout") - , EffectEditorBase() + , EffectEditorBase(EffectEditorBase::Spellmaking) { getWidget(mNameEdit, "NameEdit"); getWidget(mMagickaCost, "MagickaCost"); @@ -313,7 +315,7 @@ namespace MWGui void SpellCreationDialog::onCancelButtonClicked (MyGUI::Widget* sender) { - MWBase::Environment::get().getWindowManager()->removeGuiMode (MWGui::GM_SpellCreation); + exit(); } void SpellCreationDialog::onBuyButtonClicked (MyGUI::Widget* sender) @@ -347,7 +349,13 @@ namespace MWGui mSpell.mName = mNameEdit->getCaption(); - player.getClass().getContainerStore(player).remove(MWWorld::ContainerStore::sGoldId, boost::lexical_cast(mPriceLabel->getCaption()), player); + int price = boost::lexical_cast(mPriceLabel->getCaption()); + + player.getClass().getContainerStore(player).remove(MWWorld::ContainerStore::sGoldId, price, player); + + // add gold to NPC trading gold pool + MWMechanics::CreatureStats& npcStats = mPtr.getClass().getCreatureStats(mPtr); + npcStats.setGoldPool(npcStats.getGoldPool() + price); MWBase::Environment::get().getSoundManager()->playSound ("Item Gold Up", 1.0, 1.0); @@ -367,6 +375,11 @@ namespace MWGui center(); } + void SpellCreationDialog::exit() + { + MWBase::Environment::get().getWindowManager()->removeGuiMode (MWGui::GM_SpellCreation); + } + void SpellCreationDialog::onReferenceUnavailable () { MWBase::Environment::get().getWindowManager()->removeGuiMode (GM_Dialogue); @@ -375,6 +388,14 @@ namespace MWGui void SpellCreationDialog::notifyEffectsChanged () { + if (mEffects.empty()) + { + mMagickaCost->setCaption("0"); + mPriceLabel->setCaption("0"); + mSuccessChance->setCaption("0"); + return; + } + float y = 0; const MWWorld::ESMStore &store = @@ -382,7 +403,7 @@ namespace MWGui for (std::vector::const_iterator it = mEffects.begin(); it != mEffects.end(); ++it) { - float x = 0.5 * it->mMagnMin + it->mMagnMax; + float x = 0.5 * (it->mMagnMin + it->mMagnMax); const ESM::MagicEffect* effect = store.get().find(it->mEffectID); @@ -397,16 +418,16 @@ namespace MWGui y += x * fEffectCostMult; y = std::max(1.f,y); - if (effect->mData.mFlags & ESM::MagicEffect::CastTarget) + if (it->mRange == ESM::RT_Target) y *= 1.5; } - mSpell.mData.mCost = int(y); - ESM::EffectList effectList; effectList.mList = mEffects; mSpell.mEffects = effectList; + mSpell.mData.mCost = int(y); mSpell.mData.mType = ESM::Spell::ST_Spell; + mSpell.mData.mFlags = 0; mMagickaCost->setCaption(boost::lexical_cast(int(y))); @@ -424,10 +445,15 @@ namespace MWGui // ------------------------------------------------------------------------------------------------ - EffectEditorBase::EffectEditorBase() + EffectEditorBase::EffectEditorBase(Type type) : mAddEffectDialog() + , mAvailableEffectsList(NULL) + , mUsedEffectsView(NULL) , mSelectAttributeDialog(NULL) , mSelectSkillDialog(NULL) + , mSelectedEffect(0) + , mSelectedKnownEffectId(0) + , mType(type) { mAddEffectDialog.eventEffectAdded += MyGUI::newDelegate(this, &EffectEditorBase::onEffectAdded); mAddEffectDialog.eventEffectModified += MyGUI::newDelegate(this, &EffectEditorBase::onEffectModified); @@ -462,6 +488,13 @@ namespace MWGui const std::vector& list = spell->mEffects.mList; for (std::vector::const_iterator it2 = list.begin(); it2 != list.end(); ++it2) { + const ESM::MagicEffect * effect = MWBase::Environment::get().getWorld()->getStore().get().find(it2->mEffectID); + + // skip effects that do not allow spellmaking/enchanting + int requiredFlags = (mType == Spellmaking) ? ESM::MagicEffect::AllowSpellmaking : ESM::MagicEffect::AllowEnchanting; + if (!(effect->mData.mFlags & requiredFlags)) + continue; + if (std::find(knownEffects.begin(), knownEffects.end(), it2->mEffectID) == knownEffects.end()) knownEffects.push_back(it2->mEffectID); } @@ -494,7 +527,7 @@ namespace MWGui updateEffectsView (); } - void EffectEditorBase::setWidgets (Widgets::MWList *availableEffectsList, MyGUI::ScrollView *usedEffectsView) + void EffectEditorBase::setWidgets (Gui::MWList *availableEffectsList, MyGUI::ScrollView *usedEffectsView) { mAvailableEffectsList = availableEffectsList; mUsedEffectsView = usedEffectsView; @@ -504,16 +537,24 @@ namespace MWGui void EffectEditorBase::onSelectAttribute () { - mAddEffectDialog.setVisible(true); + const ESM::MagicEffect* effect = + MWBase::Environment::get().getWorld()->getStore().get().find(mSelectedKnownEffectId); + + mAddEffectDialog.newEffect(effect); mAddEffectDialog.setAttribute (mSelectAttributeDialog->getAttributeId()); + mAddEffectDialog.setVisible(true); MWBase::Environment::get().getWindowManager ()->removeDialog (mSelectAttributeDialog); mSelectAttributeDialog = 0; } void EffectEditorBase::onSelectSkill () { + const ESM::MagicEffect* effect = + MWBase::Environment::get().getWorld()->getStore().get().find(mSelectedKnownEffectId); + + mAddEffectDialog.newEffect(effect); + mAddEffectDialog.setSkill (mSelectSkillDialog->getSkillId()); mAddEffectDialog.setVisible(true); - mAddEffectDialog.setSkill (mSelectSkillDialog->getSkillId ()); MWBase::Environment::get().getWindowManager ()->removeDialog (mSelectSkillDialog); mSelectSkillDialog = 0; } @@ -538,11 +579,10 @@ namespace MWGui } int buttonId = *sender->getUserData(); - short effectId = mButtonMapping[buttonId]; - + mSelectedKnownEffectId = mButtonMapping[buttonId]; for (std::vector::const_iterator it = mEffects.begin(); it != mEffects.end(); ++it) { - if (it->mEffectID == effectId) + if (it->mEffectID == mSelectedKnownEffectId) { MWBase::Environment::get().getWindowManager()->messageBox ("#{sOnetypeEffectMessage}"); return; @@ -550,9 +590,7 @@ namespace MWGui } const ESM::MagicEffect* effect = - MWBase::Environment::get().getWorld()->getStore().get().find(effectId); - - mAddEffectDialog.newEffect (effect); + MWBase::Environment::get().getWorld()->getStore().get().find(mSelectedKnownEffectId); if (effect->mData.mFlags & ESM::MagicEffect::TargetSkill) { @@ -572,6 +610,7 @@ namespace MWGui } else { + mAddEffectDialog.newEffect(effect); mAddEffectDialog.setVisible(true); } } @@ -627,7 +666,10 @@ namespace MWGui ++i; } + // Canvas size must be expressed with HScroll disabled, otherwise MyGUI would expand the scroll area when the scrollbar is hidden + mUsedEffectsView->setVisibleHScroll(false); mUsedEffectsView->setCanvasSize(size); + mUsedEffectsView->setVisibleHScroll(true); notifyEffectsChanged(); } diff --git a/apps/openmw/mwgui/spellcreationdialog.hpp b/apps/openmw/mwgui/spellcreationdialog.hpp index e424d7395..ecccf3baf 100644 --- a/apps/openmw/mwgui/spellcreationdialog.hpp +++ b/apps/openmw/mwgui/spellcreationdialog.hpp @@ -1,9 +1,10 @@ #ifndef MWGUI_SPELLCREATION_H #define MWGUI_SPELLCREATION_H +#include + #include "windowbase.hpp" #include "referenceinterface.hpp" -#include "list.hpp" #include "widgets.hpp" namespace MWGui @@ -18,6 +19,7 @@ namespace MWGui EditEffectDialog(); virtual void open(); + virtual void exit(); void setSkill(int skill); void setAttribute(int attribute); @@ -84,13 +86,19 @@ namespace MWGui class EffectEditorBase { public: - EffectEditorBase(); + enum Type + { + Spellmaking, + Enchanting + }; + + EffectEditorBase(Type type); virtual ~EffectEditorBase(); protected: std::map mButtonMapping; // maps button ID to effect ID - Widgets::MWList* mAvailableEffectsList; + Gui::MWList* mAvailableEffectsList; MyGUI::ScrollView* mUsedEffectsView; EditEffectDialog mAddEffectDialog; @@ -98,6 +106,7 @@ namespace MWGui SelectSkillDialog* mSelectSkillDialog; int mSelectedEffect; + short mSelectedKnownEffectId; std::vector mEffects; @@ -116,9 +125,12 @@ namespace MWGui void updateEffectsView(); void startEditing(); - void setWidgets (Widgets::MWList* availableEffectsList, MyGUI::ScrollView* usedEffectsView); + void setWidgets (Gui::MWList* availableEffectsList, MyGUI::ScrollView* usedEffectsView); virtual void notifyEffectsChanged () {} + + private: + Type mType; }; class SpellCreationDialog : public WindowBase, public ReferenceInterface, public EffectEditorBase @@ -127,6 +139,7 @@ namespace MWGui SpellCreationDialog(); virtual void open(); + virtual void exit(); void startSpellMaking(MWWorld::Ptr actor); @@ -145,8 +158,6 @@ namespace MWGui MyGUI::Button* mCancelButton; MyGUI::TextBox* mPriceLabel; - Widgets::MWEffectList* mUsedEffectsList; - ESM::Spell mSpell; }; diff --git a/apps/openmw/mwgui/spellicons.cpp b/apps/openmw/mwgui/spellicons.cpp index 1a9e418de..dbd91ab75 100644 --- a/apps/openmw/mwgui/spellicons.cpp +++ b/apps/openmw/mwgui/spellicons.cpp @@ -5,6 +5,8 @@ #include #include +#include + #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" @@ -133,13 +135,7 @@ namespace MWGui ("ImageBox", MyGUI::IntCoord(w,2,16,16), MyGUI::Align::Default); mWidgetMap[it->first] = image; - std::string icon = effect->mIcon; - icon[icon.size()-3] = 'd'; - icon[icon.size()-2] = 'd'; - icon[icon.size()-1] = 's'; - icon = "icons\\" + icon; - - image->setImageTexture(icon); + image->setImageTexture(Misc::ResourceHelpers::correctIconPath(effect->mIcon)); std::string name = ESM::MagicEffect::effectIdToString (it->first); diff --git a/apps/openmw/mwgui/spellwindow.cpp b/apps/openmw/mwgui/spellwindow.cpp index fb5a80cc7..97ebbcde2 100644 --- a/apps/openmw/mwgui/spellwindow.cpp +++ b/apps/openmw/mwgui/spellwindow.cpp @@ -45,6 +45,7 @@ namespace MWGui , NoDrop(drag, mMainWidget) , mHeight(0) , mWidth(0) + , mWindowSize(mMainWidget->getSize()) { mSpellIcons = new SpellIcons(); @@ -66,6 +67,12 @@ namespace MWGui MWBase::Environment::get().getWindowManager()->setSpellVisibility(!mPinned); } + void SpellWindow::onTitleDoubleClicked() + { + if (!mPinned) + MWBase::Environment::get().getWindowManager()->toggleVisible(GW_Magic); + } + void SpellWindow::open() { updateSpells(); @@ -150,7 +157,7 @@ namespace MWGui for (std::vector::const_iterator it = powers.begin(); it != powers.end(); ++it) { const ESM::Spell* spell = esmStore.get().find(*it); - MyGUI::Button* t = mSpellView->createWidget("SpellText", + MyGUI::Button* t = mSpellView->createWidget("SandTextButton", MyGUI::IntCoord(4, mHeight, mWidth-8, spellHeight), MyGUI::Align::Left | MyGUI::Align::Top); t->setCaption(spell->mName); t->setTextAlign(MyGUI::Align::Left); @@ -170,7 +177,7 @@ namespace MWGui for (std::vector::const_iterator it = spellList.begin(); it != spellList.end(); ++it) { const ESM::Spell* spell = esmStore.get().find(*it); - MyGUI::Button* t = mSpellView->createWidget("SpellText", + MyGUI::Button* t = mSpellView->createWidget("SandTextButton", MyGUI::IntCoord(4, mHeight, mWidth-8, spellHeight), MyGUI::Align::Left | MyGUI::Align::Top); t->setCaption(spell->mName); t->setTextAlign(MyGUI::Align::Left); @@ -181,7 +188,7 @@ namespace MWGui t->setStateSelected(*it == MWBase::Environment::get().getWindowManager()->getSelectedSpell()); // cost / success chance - MyGUI::Button* costChance = mSpellView->createWidget("SpellText", + MyGUI::Button* costChance = mSpellView->createWidget("SandTextButton", MyGUI::IntCoord(4, mHeight, mWidth-8, spellHeight), MyGUI::Align::Left | MyGUI::Align::Top); std::string cost = boost::lexical_cast(spell->mData.mCost); std::string chance = boost::lexical_cast(int(MWMechanics::getSpellSuccessChance(*it, player))); @@ -190,6 +197,7 @@ namespace MWGui costChance->setNeedMouseFocus(false); costChance->setStateSelected(*it == MWBase::Environment::get().getWindowManager()->getSelectedSpell()); + t->setSize(mWidth-12-costChance->getTextSize().width, t->getHeight()); mHeight += spellHeight; } @@ -216,7 +224,7 @@ namespace MWGui } } - MyGUI::Button* t = mSpellView->createWidget(equipped ? "SpellText" : "SpellTextUnequipped", + MyGUI::Button* t = mSpellView->createWidget(equipped ? "SandTextButton" : "SpellTextUnequipped", MyGUI::IntCoord(4, mHeight, mWidth-8, spellHeight), MyGUI::Align::Left | MyGUI::Align::Top); t->setCaption(item.getClass().getName(item)); t->setTextAlign(MyGUI::Align::Left); @@ -230,7 +238,7 @@ namespace MWGui // cost / charge - MyGUI::Button* costCharge = mSpellView->createWidget(equipped ? "SpellText" : "SpellTextUnequipped", + MyGUI::Button* costCharge = mSpellView->createWidget(equipped ? "SandTextButton" : "SpellTextUnequipped", MyGUI::IntCoord(4, mHeight, mWidth-8, spellHeight), MyGUI::Align::Left | MyGUI::Align::Top); float enchantCost = enchant->mData.mCost; @@ -255,10 +263,15 @@ namespace MWGui if (store.getSelectedEnchantItem() != store.end()) costCharge->setStateSelected(item == *store.getSelectedEnchantItem()); + t->setSize(mWidth-12-costCharge->getTextSize().width, t->getHeight()); + mHeight += spellHeight; } + // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the scrollbar is hidden + mSpellView->setVisibleVScroll(false); mSpellView->setCanvasSize(mSpellView->getWidth(), std::max(mSpellView->getHeight(), mHeight)); + mSpellView->setVisibleVScroll(true); } void SpellWindow::addGroup(const std::string &label, const std::string& label2) @@ -287,6 +300,8 @@ namespace MWGui groupWidget2->setCaptionWithReplacing(label2); groupWidget2->setTextAlign(MyGUI::Align::Right); groupWidget2->setNeedMouseFocus(false); + + groupWidget->setSize(mWidth-8-groupWidget2->getTextSize().width, groupWidget->getHeight()); } mHeight += 24; @@ -294,7 +309,11 @@ namespace MWGui void SpellWindow::onWindowResize(MyGUI::Window* _sender) { - updateSpells(); + if (mMainWidget->getSize() != mWindowSize) + { + mWindowSize = mMainWidget->getSize(); + updateSpells(); + } } void SpellWindow::onEnchantedItemSelected(MyGUI::Widget* _sender) @@ -321,6 +340,7 @@ namespace MWGui MWBase::Environment::get().getWindowManager()->getInventoryWindow()->useItem(item); } + MWBase::Environment::get().getWindowManager()->unsetSelectedSpell(); store.setSelectedEnchantItem(it); updateSpells(); diff --git a/apps/openmw/mwgui/spellwindow.hpp b/apps/openmw/mwgui/spellwindow.hpp index 53eed1ba1..a74847b90 100644 --- a/apps/openmw/mwgui/spellwindow.hpp +++ b/apps/openmw/mwgui/spellwindow.hpp @@ -29,6 +29,8 @@ namespace MWGui int mHeight; int mWidth; + MyGUI::IntSize mWindowSize; + std::string mSpellToDelete; void addGroup(const std::string& label, const std::string& label2); @@ -42,6 +44,7 @@ namespace MWGui void onDeleteSpellAccept(); virtual void onPinToggled(); + virtual void onTitleDoubleClicked(); virtual void open(); SpellIcons* mSpellIcons; diff --git a/apps/openmw/mwgui/statswindow.cpp b/apps/openmw/mwgui/statswindow.cpp index 6ae44e314..baa779c1c 100644 --- a/apps/openmw/mwgui/statswindow.cpp +++ b/apps/openmw/mwgui/statswindow.cpp @@ -66,7 +66,7 @@ namespace MWGui mSkillWidgetMap.insert(std::pair(i, (MyGUI::TextBox*)NULL)); } - MyGUI::WindowPtr t = static_cast(mMainWidget); + MyGUI::Window* t = mMainWidget->castType(); t->eventWindowChangeCoord += MyGUI::newDelegate(this, &StatsWindow::onWindowResize); } @@ -82,12 +82,15 @@ namespace MWGui { mLeftPane->setCoord( MyGUI::IntCoord(0, 0, 0.44*window->getSize().width, window->getSize().height) ); mRightPane->setCoord( MyGUI::IntCoord(0.44*window->getSize().width, 0, 0.56*window->getSize().width, window->getSize().height) ); + // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the scrollbar is hidden + mSkillView->setVisibleVScroll(false); mSkillView->setCanvasSize (mSkillView->getWidth(), mSkillView->getCanvasSize().height); + mSkillView->setVisibleVScroll(true); } void StatsWindow::setBar(const std::string& name, const std::string& tname, int val, int max) { - MyGUI::ProgressPtr pt; + MyGUI::ProgressBar* pt; getWidget(pt, name); pt->setProgressRange(max); pt->setProgressPosition(val); @@ -99,7 +102,7 @@ namespace MWGui void StatsWindow::setPlayerName(const std::string& playerName) { - static_cast(mMainWidget)->setCaption(playerName); + mMainWidget->castType()->setCaption(playerName); adjustWindowCaption(); } @@ -321,6 +324,11 @@ namespace MWGui skillValueWidget->_setWidgetState(state); skillValueWidget->eventMouseWheel += MyGUI::newDelegate(this, &StatsWindow::onMouseWheel); + // resize dynamically according to text size + int textWidthPlusMargin = skillValueWidget->getTextSize().width + 12; + skillValueWidget->setCoord(coord2.left + coord2.width - textWidthPlusMargin, coord2.top, textWidthPlusMargin, coord2.height); + skillNameWidget->setSize(skillNameWidget->getSize() + MyGUI::IntSize(coord2.width - textWidthPlusMargin, 0)); + mSkillWidgets.push_back(skillNameWidget); mSkillWidgets.push_back(skillValueWidget); @@ -395,9 +403,24 @@ namespace MWGui mSkillWidgets[mSkillWidgets.size()-1-i]->setUserString("Caption_SkillDescription", skill->mDescription); mSkillWidgets[mSkillWidgets.size()-1-i]->setUserString("Caption_SkillAttribute", "#{sGoverningAttribute}: #{" + attr->mName + "}"); mSkillWidgets[mSkillWidgets.size()-1-i]->setUserString("ImageTexture_SkillImage", icon); - mSkillWidgets[mSkillWidgets.size()-1-i]->setUserString("Caption_SkillProgressText", boost::lexical_cast(progressPercent)+"/100"); - mSkillWidgets[mSkillWidgets.size()-1-i]->setUserString("Range_SkillProgress", "100"); - mSkillWidgets[mSkillWidgets.size()-1-i]->setUserString("RangePosition_SkillProgress", boost::lexical_cast(progressPercent)); + if (base < 100) + { + mSkillWidgets[mSkillWidgets.size()-1-i]->setUserString("Visible_SkillMaxed", "false"); + mSkillWidgets[mSkillWidgets.size()-1-i]->setUserString("UserData^Hidden_SkillMaxed", "true"); + + mSkillWidgets[mSkillWidgets.size()-1-i]->setUserString("Visible_SkillProgressVBox", "true"); + mSkillWidgets[mSkillWidgets.size()-1-i]->setUserString("UserData^Hidden_SkillProgressVBox", "false"); + + mSkillWidgets[mSkillWidgets.size()-1-i]->setUserString("Caption_SkillProgressText", boost::lexical_cast(progressPercent)+"/100"); + mSkillWidgets[mSkillWidgets.size()-1-i]->setUserString("Range_SkillProgress", "100"); + mSkillWidgets[mSkillWidgets.size()-1-i]->setUserString("RangePosition_SkillProgress", boost::lexical_cast(progressPercent)); + } else { + mSkillWidgets[mSkillWidgets.size()-1-i]->setUserString("Visible_SkillMaxed", "true"); + mSkillWidgets[mSkillWidgets.size()-1-i]->setUserString("UserData^Hidden_SkillMaxed", "false"); + + mSkillWidgets[mSkillWidgets.size()-1-i]->setUserString("Visible_SkillProgressVBox", "false"); + mSkillWidgets[mSkillWidgets.size()-1-i]->setUserString("UserData^Hidden_SkillProgressVBox", "true"); + } } mSkillWidgetMap[skillId] = widget; @@ -454,51 +477,70 @@ namespace MWGui if (!mFactions.empty()) { - // Add a line separator if there are items above - if (!mSkillWidgets.empty()) - addSeparator(coord1, coord2); - MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); const MWMechanics::NpcStats &PCstats = player.getClass().getNpcStats(player); const std::set &expelled = PCstats.getExpelled(); - addGroup(MWBase::Environment::get().getWindowManager()->getGameSettingString("sFaction", "Faction"), coord1, coord2); + bool firstFaction=true; FactionList::const_iterator end = mFactions.end(); for (FactionList::const_iterator it = mFactions.begin(); it != end; ++it) { const ESM::Faction *faction = store.get().find(it->first); + if (faction->mData.mIsHidden == 1) + continue; + + if (firstFaction) + { + // Add a line separator if there are items above + if (!mSkillWidgets.empty()) + addSeparator(coord1, coord2); + + addGroup(MWBase::Environment::get().getWindowManager()->getGameSettingString("sFaction", "Faction"), coord1, coord2); + + firstFaction = false; + } + MyGUI::Widget* w = addItem(faction->mName, coord1, coord2); std::string text; - text += std::string("#DDC79E") + faction->mName; + text += std::string("#{fontcolourhtml=header}") + faction->mName; if (expelled.find(it->first) != expelled.end()) - text += "\n#BF9959#{sExpelled}"; + text += "\n#{fontcolourhtml=normal}#{sExpelled}"; else { - text += std::string("\n#BF9959") + faction->mRanks[it->second]; + int rank = it->second; + rank = std::max(0, std::min(9, rank)); + text += std::string("\n#{fontcolourhtml=normal}") + faction->mRanks[rank]; - if (it->second < 9) + if (rank < 9) { // player doesn't have max rank yet - text += std::string("\n\n#DDC79E#{sNextRank} ") + faction->mRanks[it->second+1]; + text += std::string("\n\n#{fontcolourhtml=header}#{sNextRank} ") + faction->mRanks[rank+1]; - ESM::RankData rankData = faction->mData.mRankData[it->second+1]; + ESM::RankData rankData = faction->mData.mRankData[rank+1]; const ESM::Attribute* attr1 = store.get().find(faction->mData.mAttribute[0]); const ESM::Attribute* attr2 = store.get().find(faction->mData.mAttribute[1]); - text += "\n#BF9959#{" + attr1->mName + "}: " + boost::lexical_cast(rankData.mAttribute1) + text += "\n#{fontcolourhtml=normal}#{" + attr1->mName + "}: " + boost::lexical_cast(rankData.mAttribute1) + ", #{" + attr2->mName + "}: " + boost::lexical_cast(rankData.mAttribute2); - text += "\n\n#DDC79E#{sFavoriteSkills}"; - text += "\n#BF9959"; - for (int i=0; i<6; ++i) + text += "\n\n#{fontcolourhtml=header}#{sFavoriteSkills}"; + text += "\n#{fontcolourhtml=normal}"; + bool firstSkill = true; + for (int i=0; i<7; ++i) { - text += "#{"+ESM::Skill::sSkillNameIds[faction->mData.mSkills[i]]+"}"; - if (i<5) - text += ", "; + if (faction->mData.mSkills[i] != -1) + { + if (!firstSkill) + text += ", "; + + firstSkill = false; + + text += "#{"+ESM::Skill::sSkillNameIds[faction->mData.mSkills[i]]+"}"; + } } text += "\n"; @@ -554,11 +596,20 @@ namespace MWGui mSkillWidgets[mSkillWidgets.size()-1-i]->setUserString("Caption_Text", "#{sCrimeHelp}"); } + // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the scrollbar is hidden + mSkillView->setVisibleVScroll(false); mSkillView->setCanvasSize (mSkillView->getWidth(), std::max(mSkillView->getHeight(), coord1.top)); + mSkillView->setVisibleVScroll(true); } void StatsWindow::onPinToggled() { MWBase::Environment::get().getWindowManager()->setHMSVisibility(!mPinned); } + + void StatsWindow::onTitleDoubleClicked() + { + if (!mPinned) + MWBase::Environment::get().getWindowManager()->toggleVisible(GW_Stats); + } } diff --git a/apps/openmw/mwgui/statswindow.hpp b/apps/openmw/mwgui/statswindow.hpp index d90c16be9..f41995ac0 100644 --- a/apps/openmw/mwgui/statswindow.hpp +++ b/apps/openmw/mwgui/statswindow.hpp @@ -37,7 +37,7 @@ namespace MWGui void setBounty (int bounty) { if (bounty != mBounty) mChanged = true; this->mBounty = bounty; } void updateSkillArea(); - virtual void open() { onWindowResize(static_cast(mMainWidget)); } + virtual void open() { onWindowResize(mMainWidget->castType()); } private: void addSkills(const SkillList &skills, const std::string &titleId, const std::string &titleDefault, MyGUI::IntCoord &coord1, MyGUI::IntCoord &coord2); @@ -74,6 +74,7 @@ namespace MWGui protected: virtual void onPinToggled(); + virtual void onTitleDoubleClicked(); }; } #endif diff --git a/apps/openmw/mwgui/textinput.hpp b/apps/openmw/mwgui/textinput.hpp index 1ed80fc1e..c83e3ffa9 100644 --- a/apps/openmw/mwgui/textinput.hpp +++ b/apps/openmw/mwgui/textinput.hpp @@ -22,6 +22,11 @@ namespace MWGui void setTextLabel(const std::string &label); virtual void open(); + /** Event : Dialog finished, OK button clicked.\n + signature : void method()\n + */ + EventHandle_WindowBase eventDone; + protected: void onOkClicked(MyGUI::Widget* _sender); void onTextAccepted(MyGUI::Edit* _sender); diff --git a/apps/openmw/mwgui/tooltips.cpp b/apps/openmw/mwgui/tooltips.cpp index aeb79a938..4608010ac 100644 --- a/apps/openmw/mwgui/tooltips.cpp +++ b/apps/openmw/mwgui/tooltips.cpp @@ -4,11 +4,14 @@ #include +#include + #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwworld/class.hpp" +#include "../mwmechanics/spellcasting.hpp" #include "mapwindow.hpp" #include "inventorywindow.hpp" @@ -17,6 +20,7 @@ namespace MWGui { + std::string ToolTips::sSchoolNames[] = {"#{sSchoolAlteration}", "#{sSchoolConjuration}", "#{sSchoolDestruction}", "#{sSchoolIllusion}", "#{sSchoolMysticism}", "#{sSchoolRestoration}"}; ToolTips::ToolTips() : Layout("openmw_tooltips.layout") @@ -84,8 +88,6 @@ namespace MWGui || (MWBase::Environment::get().getWindowManager()->getMode() == GM_Container) || (MWBase::Environment::get().getWindowManager()->getMode() == GM_Inventory))) { - mFocusObject = MWBase::Environment::get().getWorld()->getFacedObject(); - if (mFocusObject.isEmpty ()) return; @@ -97,7 +99,9 @@ namespace MWGui setCoord(0, 0, 300, 300); mDynamicToolTipBox->setVisible(true); ToolTipInfo info; - info.caption=mFocusObject.getCellRef().getRefId(); + info.caption = mFocusObject.getClass().getName(mFocusObject); + if (info.caption.empty()) + info.caption=mFocusObject.getCellRef().getRefId(); info.icon=""; tooltipSize = createToolTip(info); } @@ -218,6 +222,12 @@ namespace MWGui params.mNoTarget = false; effects.push_back(params); } + if (MWMechanics::spellIncreasesSkill(spell)) // display school of spells that contribute to skill progress + { + MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); + int school = MWMechanics::getSpellSchool(spell, player); + info.text = "#{sSchool}: " + sSchoolNames[school]; + } info.effects = effects; tooltipSize = createToolTip(info); } @@ -233,19 +243,26 @@ namespace MWGui for (std::map::iterator it = userStrings.begin(); it != userStrings.end(); ++it) { - if (it->first == "ToolTipType" - || it->first == "ToolTipLayout" - || it->first == "IsMarker") - continue; - - size_t underscorePos = it->first.find("_"); - std::string propertyKey = it->first.substr(0, underscorePos); + if (underscorePos == std::string::npos) + continue; + std::string key = it->first.substr(0, underscorePos); std::string widgetName = it->first.substr(underscorePos+1, it->first.size()-(underscorePos+1)); + std::string type = "Property"; + size_t caretPos = key.find("^"); + if (caretPos != std::string::npos) + { + type = key.substr(0, caretPos); + key.erase(key.begin(), key.begin() + caretPos + 1); + } + MyGUI::Widget* w; getWidget(w, widgetName); - w->setProperty(propertyKey, it->second); + if (type == "Property") + w->setProperty(key, it->second); + else if (type == "UserData") + w->setUserString(key, it->second); } tooltipSize = tooltip->getSize(); @@ -323,20 +340,6 @@ namespace MWGui return tooltipSize; } - void ToolTips::findImageExtension(std::string& image) - { - int len = image.size(); - if (len < 4) return; - - if (!Ogre::ResourceGroupManager::getSingleton().resourceExists(Ogre::ResourceGroupManager::AUTODETECT_RESOURCE_GROUP_NAME, image)) - { - // Change texture extension to .dds - image[len-3] = 'd'; - image[len-2] = 'd'; - image[len-1] = 's'; - } - } - MyGUI::IntSize ToolTips::createToolTip(const MWGui::ToolTipInfo& info) { mDynamicToolTipBox->setVisible(true); @@ -375,8 +378,7 @@ namespace MWGui const int imageCaptionHPadding = (caption != "" ? 8 : 0); const int imageCaptionVPadding = (caption != "" ? 4 : 0); - std::string realImage = "icons\\" + image; - findImageExtension(realImage); + std::string realImage = Misc::ResourceHelpers::correctIconPath(image); MyGUI::EditBox* captionWidget = mDynamicToolTipBox->createWidget("NormalText", MyGUI::IntCoord(0, 0, 300, 300), MyGUI::Align::Left | MyGUI::Align::Top, "ToolTipCaption"); captionWidget->setProperty("Static", "true"); @@ -400,7 +402,7 @@ namespace MWGui if (!info.effects.empty()) { MyGUI::Widget* effectArea = mDynamicToolTipBox->createWidget("", - MyGUI::IntCoord(0, totalSize.height, 300, 300-totalSize.height), + MyGUI::IntCoord(padding.left, totalSize.height, 300-padding.left, 300-totalSize.height), MyGUI::Align::Stretch, "ToolTipEffectArea"); MyGUI::IntCoord coord(0, 6, totalSize.width, 24); @@ -419,7 +421,7 @@ namespace MWGui { assert(enchant); MyGUI::Widget* enchantArea = mDynamicToolTipBox->createWidget("", - MyGUI::IntCoord(0, totalSize.height, 300, 300-totalSize.height), + MyGUI::IntCoord(padding.left, totalSize.height, 300-padding.left, 300-totalSize.height), MyGUI::Align::Stretch, "ToolTipEnchantArea"); MyGUI::IntCoord coord(0, 6, totalSize.width, 24); @@ -512,7 +514,11 @@ namespace MWGui std::string ToolTips::toString(const float value) { std::ostringstream stream; - stream << std::setprecision(3) << value; + + if (value != int(value)) + stream << std::setprecision(3); + + stream << value; return stream.str(); } @@ -547,6 +553,17 @@ namespace MWGui return " (" + boost::lexical_cast(value) + ")"; } + std::string ToolTips::getCellRefString(const MWWorld::CellRef& cellref) + { + std::string ret; + ret += getMiscString(cellref.getOwner(), "Owner"); + ret += getMiscString(cellref.getFaction(), "Faction"); + if (cellref.getFactionRank() > 0) + ret += getValueString(cellref.getFactionRank(), "Rank"); + ret += getMiscString(cellref.getGlobalVariable(), "Global"); + return ret; + } + bool ToolTips::toggleFullHelp() { mFullHelp = !mFullHelp; @@ -633,13 +650,11 @@ namespace MWGui widget->setUserString("ToolTipType", "Layout"); widget->setUserString("ToolTipLayout", "BirthSignToolTip"); - std::string image = sign->mTexture; - image.replace(image.size()-3, 3, "dds"); - widget->setUserString("ImageTexture_BirthSignImage", "textures\\" + image); + widget->setUserString("ImageTexture_BirthSignImage", Misc::ResourceHelpers::correctTexturePath(sign->mTexture)); std::string text; text += sign->mName; - text += "\n#BF9959" + sign->mDescription; + text += "\n#{fontcolourhtml=normal}" + sign->mDescription; std::vector abilities, powers, spells; @@ -679,13 +694,13 @@ namespace MWGui { if (it == categories[category].spells.begin()) { - text += std::string("\n#DDC79E") + std::string("#{") + categories[category].label + "}"; + text += std::string("\n#{fontcolourhtml=header}") + std::string("#{") + categories[category].label + "}"; } const std::string &spellId = *it; const ESM::Spell *spell = store.get().find(spellId); - text += "\n#BF9959" + spell->mName; + text += "\n#{fontcolourhtml=normal}" + spell->mName; } } @@ -728,29 +743,15 @@ namespace MWGui const std::string &name = ESM::MagicEffect::effectIdToString (id); std::string icon = effect->mIcon; - - int slashPos = icon.find("\\"); + int slashPos = icon.rfind('\\'); icon.insert(slashPos+1, "b_"); - - icon[icon.size()-3] = 'd'; - icon[icon.size()-2] = 'd'; - icon[icon.size()-1] = 's'; - - icon = "icons\\" + icon; - - std::vector schools; - schools.push_back ("#{sSchoolAlteration}"); - schools.push_back ("#{sSchoolConjuration}"); - schools.push_back ("#{sSchoolDestruction}"); - schools.push_back ("#{sSchoolIllusion}"); - schools.push_back ("#{sSchoolMysticism}"); - schools.push_back ("#{sSchoolRestoration}"); + icon = Misc::ResourceHelpers::correctIconPath(icon); widget->setUserString("ToolTipType", "Layout"); widget->setUserString("ToolTipLayout", "MagicEffectToolTip"); widget->setUserString("Caption_MagicEffectName", "#{" + name + "}"); widget->setUserString("Caption_MagicEffectDescription", effect->mDescription); - widget->setUserString("Caption_MagicEffectSchool", "#{sSchool}: " + schools[effect->mData.mSchool]); + widget->setUserString("Caption_MagicEffectSchool", "#{sSchool}: " + sSchoolNames[effect->mData.mSchool]); widget->setUserString("ImageTexture_MagicEffectImage", icon); } diff --git a/apps/openmw/mwgui/tooltips.hpp b/apps/openmw/mwgui/tooltips.hpp index 4e73cc555..ffbb35e04 100644 --- a/apps/openmw/mwgui/tooltips.hpp +++ b/apps/openmw/mwgui/tooltips.hpp @@ -66,6 +66,9 @@ namespace MWGui static std::string getCountString(const int value); ///< @return blank string if count is 1, or else " (value)" + static std::string getCellRefString(const MWWorld::CellRef& cellref); + ///< Returns a string containing debug tooltip information about the given cellref. + // these do not create an actual tooltip, but they fill in the data that is required so the tooltip // system knows what to show in case this widget is hovered static void createSkillToolTip(MyGUI::Widget* widget, int skillId); @@ -81,8 +84,6 @@ namespace MWGui MWWorld::Ptr mFocusObject; - void findImageExtension(std::string& image); - MyGUI::IntSize getToolTipViaPtr (bool image=true); ///< @return requested tooltip size @@ -95,6 +96,8 @@ namespace MWGui /// Adjust position for a tooltip so that it doesn't leave the screen and does not obscure the mouse cursor void position(MyGUI::IntPoint& position, MyGUI::IntSize size, MyGUI::IntSize viewportSize); + static std::string sSchoolNames[6]; + int mHorizontalScrollIndex; diff --git a/apps/openmw/mwgui/tradeitemmodel.cpp b/apps/openmw/mwgui/tradeitemmodel.cpp index fe43eb548..3abfac997 100644 --- a/apps/openmw/mwgui/tradeitemmodel.cpp +++ b/apps/openmw/mwgui/tradeitemmodel.cpp @@ -35,7 +35,7 @@ namespace MWGui bool found = false; for (; it != out.end(); ++it) { - if (it->stacks(item)) + if (it->mBase == item.mBase) { it->mCount += item.mCount; found = true; @@ -52,7 +52,7 @@ namespace MWGui bool found = false; for (; it != out.end(); ++it) { - if (it->stacks(item)) + if (it->mBase == item.mBase) { if (it->mCount < count) throw std::runtime_error("Not enough borrowed items to return"); @@ -93,6 +93,21 @@ namespace MWGui unborrowImpl(item, count, mBorrowedFromUs); } + void TradeItemModel::adjustEncumbrance(float &encumbrance) + { + for (std::vector::iterator it = mBorrowedToUs.begin(); it != mBorrowedToUs.end(); ++it) + { + MWWorld::Ptr item = it->mBase; + encumbrance += item.getClass().getWeight(item) * it->mCount; + } + for (std::vector::iterator it = mBorrowedFromUs.begin(); it != mBorrowedFromUs.end(); ++it) + { + MWWorld::Ptr item = it->mBase; + encumbrance -= item.getClass().getWeight(item) * it->mCount; + } + encumbrance = std::max(0.f, encumbrance); + } + void TradeItemModel::abort() { mBorrowedFromUs.clear(); @@ -114,7 +129,7 @@ namespace MWGui size_t i=0; for (; igetItemCount(); ++i) { - if (it->stacks(sourceModel->getItem(i))) + if (it->mBase == sourceModel->getItem(i).mBase) break; } if (i == sourceModel->getItemCount()) @@ -154,11 +169,8 @@ namespace MWGui continue; // Bound items may not be bought - if (item.mBase.getCellRef().getRefId().size() > 6 - && item.mBase.getCellRef().getRefId().substr(0,6) == "bound_") - { + if (item.mFlags & ItemStack::Flag_Bound) continue; - } // don't show equipped items if(mMerchant.getClass().hasInventoryStore(mMerchant)) @@ -182,7 +194,7 @@ namespace MWGui std::vector::iterator it = mBorrowedFromUs.begin(); for (; it != mBorrowedFromUs.end(); ++it) { - if (it->stacks(item)) + if (it->mBase == item.mBase) { if (item.mCount < it->mCount) throw std::runtime_error("Lent more items than present"); diff --git a/apps/openmw/mwgui/tradeitemmodel.hpp b/apps/openmw/mwgui/tradeitemmodel.hpp index 5cfaaafc7..1bfee9b2a 100644 --- a/apps/openmw/mwgui/tradeitemmodel.hpp +++ b/apps/openmw/mwgui/tradeitemmodel.hpp @@ -34,6 +34,10 @@ namespace MWGui /// Aborts trade void abort(); + /// Adjusts the given encumbrance by adding weight for items that have been lent to us, + /// and removing weight for items we've lent to someone else. + void adjustEncumbrance (float& encumbrance); + std::vector getItemsBorrowedToUs(); private: diff --git a/apps/openmw/mwgui/tradewindow.cpp b/apps/openmw/mwgui/tradewindow.cpp index 558e955f0..0f98598fd 100644 --- a/apps/openmw/mwgui/tradewindow.cpp +++ b/apps/openmw/mwgui/tradewindow.cpp @@ -2,6 +2,8 @@ #include +#include + #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/soundmanager.hpp" @@ -22,6 +24,20 @@ #include "tradeitemmodel.hpp" #include "countdialog.hpp" #include "dialogue.hpp" +#include "controllers.hpp" + +namespace +{ + + int getEffectiveValue (MWWorld::Ptr item, int count) + { + int price = item.getClass().getValue(item) * count; + if (item.getClass().hasItemHealth(item)) + price *= (static_cast(item.getClass().getItemHealth(item)) / item.getClass().getItemMaxHealth(item)); + return price; + } + +} namespace MWGui { @@ -31,8 +47,6 @@ namespace MWGui TradeWindow::TradeWindow() : WindowBase("openmw_trade_window.layout") , mCurrentBalance(0) - , mBalanceButtonsState(BBS_None) - , mBalanceChangePause(0.0) , mItemToSell(-1) , mTradeModel(NULL) , mSortModel(NULL) @@ -74,6 +88,9 @@ namespace MWGui mDecreaseButton->eventMouseButtonPressed += MyGUI::newDelegate(this, &TradeWindow::onDecreaseButtonPressed); mDecreaseButton->eventMouseButtonReleased += MyGUI::newDelegate(this, &TradeWindow::onBalanceButtonReleased); + mTotalBalance->eventValueChanged += MyGUI::newDelegate(this, &TradeWindow::onBalanceValueChanged); + mTotalBalance->setMinValue(INT_MIN+1); // disallow INT_MIN since abs(INT_MIN) is undefined + setCoord(400, 0, 400, 300); } @@ -84,8 +101,6 @@ namespace MWGui mCurrentBalance = 0; mCurrentMerchantOffer = 0; - restock(); - std::vector itemSources; MWBase::Environment::get().getWorld()->getContainersOwnedBy(actor, itemSources); @@ -126,7 +141,7 @@ namespace MWGui mFilterMagic->setStateSelected(false); mFilterMisc->setStateSelected(false); - static_cast(_sender)->setStateSelected(true); + _sender->castType()->setStateSelected(true); mItemView->update(); } @@ -136,6 +151,13 @@ namespace MWGui return mPtr.getClass().getServices(mPtr); } + void TradeWindow::exit() + { + mTradeModel->abort(); + MWBase::Environment::get().getWindowManager()->getInventoryWindow()->getTradeModel()->abort(); + MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Barter); + } + void TradeWindow::onItemSelected (int index) { const ItemStack& item = mSortModel->getItem(index); @@ -220,21 +242,6 @@ namespace MWGui } } - void TradeWindow::onFrame(float frameDuration) - { - if (!mMainWidget->getVisible() || mBalanceButtonsState == BBS_None) - return; - - mBalanceChangePause -= frameDuration; - if (mBalanceChangePause < 0.0) { - mBalanceChangePause += sBalanceChangeInterval; - if (mBalanceButtonsState == BBS_Increase) - onIncreaseButtonTriggered(); - else if (mBalanceButtonsState == BBS_Decrease) - onDecreaseButtonTriggered(); - } - } - void TradeWindow::onOfferButtonClicked(MyGUI::Widget* _sender) { TradeItemModel* playerItemModel = MWBase::Environment::get().getWindowManager()->getInventoryWindow()->getTradeModel(); @@ -288,11 +295,14 @@ namespace MWGui it->mBase.getClass().getValue(it->mBase) * it->mCount); onCancelButtonClicked(mCancelButton); - MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Dialogue); + MWBase::Environment::get().getDialogueManager()->goodbyeSelected(); return; } } + // Is the player buying? + bool buying = (mCurrentMerchantOffer < 0); + if(mCurrentBalance > mCurrentMerchantOffer) { //if npc is a creature: reject (no haggle) @@ -306,7 +316,7 @@ namespace MWGui int a = abs(mCurrentMerchantOffer); int b = abs(mCurrentBalance); int d = 0; - if (mCurrentBalance<0) + if (buying) d = int(100 * (a - b) / a); else d = int(100 * (b - a) / a); @@ -317,23 +327,24 @@ namespace MWGui const MWMechanics::CreatureStats &sellerStats = mPtr.getClass().getCreatureStats(mPtr); const MWMechanics::CreatureStats &playerStats = player.getClass().getCreatureStats(player); - float a1 = std::min(player.getClass().getSkill(player, ESM::Skill::Mercantile), 100); - float b1 = std::min(0.1f * playerStats.getAttribute(ESM::Attribute::Luck).getModified(), 10.f); - float c1 = std::min(0.2f * playerStats.getAttribute(ESM::Attribute::Personality).getModified(), 10.f); - float d1 = std::min(mPtr.getClass().getSkill(mPtr, ESM::Skill::Mercantile), 100); - float e1 = std::min(0.1f * sellerStats.getAttribute(ESM::Attribute::Luck).getModified(), 10.f); - float f1 = std::min(0.2f * sellerStats.getAttribute(ESM::Attribute::Personality).getModified(), 10.f); + float a1 = player.getClass().getSkill(player, ESM::Skill::Mercantile); + float b1 = 0.1f * playerStats.getAttribute(ESM::Attribute::Luck).getModified(); + float c1 = 0.2f * playerStats.getAttribute(ESM::Attribute::Personality).getModified(); + float d1 = mPtr.getClass().getSkill(mPtr, ESM::Skill::Mercantile); + float e1 = 0.1f * sellerStats.getAttribute(ESM::Attribute::Luck).getModified(); + float f1 = 0.2f * sellerStats.getAttribute(ESM::Attribute::Personality).getModified(); - float pcTerm = (clampedDisposition - 50 + a1 + b1 + c1) * playerStats.getFatigueTerm(); + float dispositionTerm = gmst.find("fDispositionMod")->getFloat() * (clampedDisposition - 50); + float pcTerm = (dispositionTerm - 50 + a1 + b1 + c1) * playerStats.getFatigueTerm(); float npcTerm = (d1 + e1 + f1) * sellerStats.getFatigueTerm(); float x = gmst.find("fBargainOfferMulti")->getFloat() * d + gmst.find("fBargainOfferBase")->getFloat(); - if (mCurrentBalance<0) + if (buying) x += abs(int(pcTerm - npcTerm)); else x += abs(int(npcTerm - pcTerm)); int roll = std::rand()%100 + 1; - if(roll > x) //trade refused + if(roll > x || (mCurrentMerchantOffer < 0) != (mCurrentBalance < 0)) //trade refused { MWBase::Environment::get().getWindowManager()-> messageBox("#{sNotifyMessage9}"); @@ -345,7 +356,15 @@ namespace MWGui } //skill use! - player.getClass().skillUsageSucceeded(player, ESM::Skill::Mercantile, 0); + float skillGain = 0.f; + int finalPrice = std::abs(mCurrentBalance); + int initialMerchantOffer = std::abs(mCurrentMerchantOffer); + if (!buying && (finalPrice > initialMerchantOffer) && finalPrice > 0) + skillGain = int(100 * (finalPrice - initialMerchantOffer) / float(finalPrice)); + else if (buying && (finalPrice < initialMerchantOffer) && initialMerchantOffer > 0) + skillGain = int(100 * (initialMerchantOffer - finalPrice) / float(initialMerchantOffer)); + + player.getClass().skillUsageSucceeded(player, ESM::Skill::Mercantile, 0, skillGain); } int iBarterSuccessDisposition = gmst.find("iBarterSuccessDisposition")->getInt(); @@ -375,9 +394,7 @@ namespace MWGui void TradeWindow::onCancelButtonClicked(MyGUI::Widget* _sender) { - mTradeModel->abort(); - MWBase::Environment::get().getWindowManager()->getInventoryWindow()->getTradeModel()->abort(); - MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Barter); + exit(); } void TradeWindow::onMaxSaleButtonClicked(MyGUI::Widget* _sender) @@ -386,27 +403,55 @@ namespace MWGui updateLabels(); } + void TradeWindow::addRepeatController(MyGUI::Widget *widget) + { + MyGUI::ControllerItem* item = MyGUI::ControllerManager::getInstance().createItem(Controllers::ControllerRepeatEvent::getClassTypeName()); + Controllers::ControllerRepeatEvent* controller = item->castType(); + controller->eventRepeatClick += MyGUI::newDelegate(this, &TradeWindow::onRepeatClick); + controller->setRepeat(sBalanceChangeInitialPause, sBalanceChangeInterval); + MyGUI::ControllerManager::getInstance().addItem(widget, controller); + } + void TradeWindow::onIncreaseButtonPressed(MyGUI::Widget* _sender, int _left, int _top, MyGUI::MouseButton _id) { - mBalanceButtonsState = BBS_Increase; - mBalanceChangePause = sBalanceChangeInitialPause; + addRepeatController(_sender); onIncreaseButtonTriggered(); } void TradeWindow::onDecreaseButtonPressed(MyGUI::Widget* _sender, int _left, int _top, MyGUI::MouseButton _id) { - mBalanceButtonsState = BBS_Decrease; - mBalanceChangePause = sBalanceChangeInitialPause; + addRepeatController(_sender); onDecreaseButtonTriggered(); } + void TradeWindow::onRepeatClick(MyGUI::Widget* widget, MyGUI::ControllerItem* controller) + { + if (widget == mIncreaseButton) + onIncreaseButtonTriggered(); + else if (widget == mDecreaseButton) + onDecreaseButtonTriggered(); + } + void TradeWindow::onBalanceButtonReleased(MyGUI::Widget *_sender, int _left, int _top, MyGUI::MouseButton _id) { - mBalanceButtonsState = BBS_None; + MyGUI::ControllerManager::getInstance().removeItem(_sender); + } + + void TradeWindow::onBalanceValueChanged(int value) + { + // Entering a "-" sign inverts the buying/selling state + mCurrentBalance = (mCurrentBalance >= 0 ? 1 : -1) * value; + updateLabels(); + + if (value != std::abs(value)) + mTotalBalance->setValue(std::abs(value)); } void TradeWindow::onIncreaseButtonTriggered() { + // prevent overflows, and prevent entering INT_MIN since abs(INT_MIN) is undefined + if (mCurrentBalance == INT_MAX || mCurrentBalance == INT_MIN+1) + return; if(mCurrentBalance<=-1) mCurrentBalance -= 1; if(mCurrentBalance>=1) mCurrentBalance += 1; updateLabels(); @@ -429,35 +474,49 @@ namespace MWGui if (mCurrentBalance > 0) { mTotalBalanceLabel->setCaptionWithReplacing("#{sTotalSold}"); - mTotalBalance->setCaption(boost::lexical_cast(mCurrentBalance)); } else { mTotalBalanceLabel->setCaptionWithReplacing("#{sTotalCost}"); - mTotalBalance->setCaption(boost::lexical_cast(-mCurrentBalance)); } + mTotalBalance->setValue(std::abs(mCurrentBalance)); + mMerchantGold->setCaptionWithReplacing("#{sSellerGold} " + boost::lexical_cast(getMerchantGold())); } - void TradeWindow::sellToNpc(const MWWorld::Ptr& item, int count, bool boughtItem) + void TradeWindow::updateOffer() { - int diff = MWBase::Environment::get().getMechanicsManager()->getBarterOffer(mPtr, item.getClass().getValue(item) * count, boughtItem); + TradeItemModel* playerTradeModel = MWBase::Environment::get().getWindowManager()->getInventoryWindow()->getTradeModel(); - mCurrentBalance += diff; - mCurrentMerchantOffer += diff; + int merchantOffer = 0; + + std::vector playerBorrowed = playerTradeModel->getItemsBorrowedToUs(); + for (std::vector::const_iterator it = playerBorrowed.begin(); it != playerBorrowed.end(); ++it) + { + merchantOffer -= MWBase::Environment::get().getMechanicsManager()->getBarterOffer(mPtr, getEffectiveValue(it->mBase, it->mCount), true); + } + + std::vector merchantBorrowed = mTradeModel->getItemsBorrowedToUs(); + for (std::vector::const_iterator it = merchantBorrowed.begin(); it != merchantBorrowed.end(); ++it) + { + merchantOffer += MWBase::Environment::get().getMechanicsManager()->getBarterOffer(mPtr, getEffectiveValue(it->mBase, it->mCount), false); + } + int diff = merchantOffer - mCurrentMerchantOffer; + mCurrentMerchantOffer = merchantOffer; + mCurrentBalance += diff; updateLabels(); } - void TradeWindow::buyFromNpc(const MWWorld::Ptr& item, int count, bool soldItem) + void TradeWindow::sellToNpc(const MWWorld::Ptr& item, int count, bool boughtItem) { - int diff = MWBase::Environment::get().getMechanicsManager()->getBarterOffer(mPtr, item.getClass().getValue(item) * count, !soldItem); - - mCurrentBalance -= diff; - mCurrentMerchantOffer -= diff; + updateOffer(); + } - updateLabels(); + void TradeWindow::buyFromNpc(const MWWorld::Ptr& item, int count, bool soldItem) + { + updateOffer(); } void TradeWindow::onReferenceUnavailable() @@ -473,26 +532,11 @@ namespace MWGui return merchantGold; } - void TradeWindow::restock() + void TradeWindow::resetReference() { - MWMechanics::CreatureStats &sellerStats = mPtr.getClass().getCreatureStats(mPtr); - float delay = MWBase::Environment::get().getWorld()->getStore().get().find("fBarterGoldResetDelay")->getFloat(); - - if (MWBase::Environment::get().getWorld()->getTimeStamp() >= sellerStats.getLastRestockTime() + delay) - { - sellerStats.setGoldPool(mPtr.getClass().getBaseGold(mPtr)); - - mPtr.getClass().restock(mPtr); - - // Also restock any containers owned by this merchant, which are also available to buy in the trade window - std::vector itemSources; - MWBase::Environment::get().getWorld()->getContainersOwnedBy(mPtr, itemSources); - for (std::vector::iterator it = itemSources.begin(); it != itemSources.end(); ++it) - { - it->getClass().restock(*it); - } - - sellerStats.setLastRestockTime(MWBase::Environment::get().getWorld()->getTimeStamp()); - } + ReferenceInterface::resetReference(); + mItemView->setModel(NULL); + mTradeModel = NULL; + mSortModel = NULL; } } diff --git a/apps/openmw/mwgui/tradewindow.hpp b/apps/openmw/mwgui/tradewindow.hpp index 2da58e72e..47de9215a 100644 --- a/apps/openmw/mwgui/tradewindow.hpp +++ b/apps/openmw/mwgui/tradewindow.hpp @@ -3,10 +3,9 @@ #include "container.hpp" -namespace MyGUI +namespace Gui { - class Gui; - class Widget; + class NumericEditBox; } namespace MWGui @@ -28,13 +27,14 @@ namespace MWGui void startTrade(const MWWorld::Ptr& actor); - void onFrame(float frameDuration); - void borrowItem (int index, size_t count); void returnItem (int index, size_t count); int getMerchantServices(); + virtual void exit(); + + virtual void resetReference(); private: ItemView* mItemView; @@ -53,7 +53,7 @@ namespace MWGui MyGUI::Button* mIncreaseButton; MyGUI::Button* mDecreaseButton; MyGUI::TextBox* mTotalBalanceLabel; - MyGUI::TextBox* mTotalBalance; + Gui::NumericEditBox* mTotalBalance; MyGUI::Widget* mBottomPane; @@ -68,17 +68,11 @@ namespace MWGui int mCurrentBalance; int mCurrentMerchantOffer; - enum BalanceButtonsState { - BBS_None, - BBS_Increase, - BBS_Decrease - } mBalanceButtonsState; - /// pause before next balance change will trigger while user holds +/- button pressed - float mBalanceChangePause; - void sellToNpc(const MWWorld::Ptr& item, int count, bool boughtItem); ///< only used for adjusting the gold balance void buyFromNpc(const MWWorld::Ptr& item, int count, bool soldItem); ///< only used for adjusting the gold balance + void updateOffer(); + void onItemSelected (int index); void sellItem (MyGUI::Widget* sender, int count); @@ -89,6 +83,10 @@ namespace MWGui void onIncreaseButtonPressed(MyGUI::Widget* _sender, int _left, int _top, MyGUI::MouseButton _id); void onDecreaseButtonPressed(MyGUI::Widget* _sender, int _left, int _top, MyGUI::MouseButton _id); void onBalanceButtonReleased(MyGUI::Widget* _sender, int _left, int _top, MyGUI::MouseButton _id); + void onBalanceValueChanged(int value); + void onRepeatClick(MyGUI::Widget* widget, MyGUI::ControllerItem* controller); + + void addRepeatController(MyGUI::Widget* widget); void onIncreaseButtonTriggered(); void onDecreaseButtonTriggered(); @@ -100,8 +98,6 @@ namespace MWGui virtual void onReferenceUnavailable(); int getMerchantGold(); - - void restock(); }; } diff --git a/apps/openmw/mwgui/trainingwindow.cpp b/apps/openmw/mwgui/trainingwindow.cpp index 34873b9bc..6ff5ae35f 100644 --- a/apps/openmw/mwgui/trainingwindow.cpp +++ b/apps/openmw/mwgui/trainingwindow.cpp @@ -2,12 +2,11 @@ #include -#include - #include "../mwbase/windowmanager.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/mechanicsmanager.hpp" +#include "../mwbase/dialoguemanager.hpp" #include "../mwworld/class.hpp" #include "../mwworld/containerstore.hpp" @@ -16,6 +15,24 @@ #include "tooltips.hpp" +namespace +{ +// Sorts a container descending by skill value. If skill value is equal, sorts ascending by skill ID. +// pair +bool sortSkills (const std::pair& left, const std::pair& right) +{ + if (left == right) + return false; + + if (left.second > right.second) + return true; + else if (left.second < right.second) + return false; + + return left.first < right.first; +} +} + namespace MWGui { @@ -35,6 +52,11 @@ namespace MWGui center(); } + void TrainingWindow::exit() + { + MWBase::Environment::get().getWindowManager()->removeGuiMode (GM_Training); + } + void TrainingWindow::startTraining (MWWorld::Ptr actor) { mPtr = actor; @@ -47,29 +69,17 @@ namespace MWGui MWMechanics::NpcStats& npcStats = actor.getClass().getNpcStats (actor); // NPC can train you in his best 3 skills - std::vector< std::pair > bestSkills; - bestSkills.push_back (std::make_pair(-1, -1)); - bestSkills.push_back (std::make_pair(-1, -1)); - bestSkills.push_back (std::make_pair(-1, -1)); + std::vector< std::pair > skills; for (int i=0; i bestSkills[j].second) - { - if (j<2) - { - bestSkills[j+1] = bestSkills[j]; - } - bestSkills[j] = std::make_pair(i, value); - break; - } - } + skills.push_back(std::make_pair(i, value)); } + std::sort(skills.begin(), skills.end(), sortSkills); + MyGUI::EnumeratorWidgetPtr widgets = mTrainingOptions->getEnumerator (); MyGUI::Gui::getInstance ().destroyWidgets (widgets); @@ -81,20 +91,19 @@ namespace MWGui for (int i=0; i<3; ++i) { int price = MWBase::Environment::get().getMechanicsManager()->getBarterOffer - (mPtr,pcStats.getSkill (bestSkills[i].first).getBase() * gmst.find("iTrainingMod")->getInt (),true); + (mPtr,pcStats.getSkill (skills[i].first).getBase() * gmst.find("iTrainingMod")->getInt (),true); - MyGUI::Button* button = mTrainingOptions->createWidget("SandTextButton", + MyGUI::Button* button = mTrainingOptions->createWidget(price <= playerGold ? "SandTextButton" : "SandTextButtonDisabled", // can't use setEnabled since that removes tooltip MyGUI::IntCoord(5, 5+i*18, mTrainingOptions->getWidth()-10, 18), MyGUI::Align::Default); - button->setEnabled(price <= playerGold); - button->setUserData(bestSkills[i].first); + button->setUserData(skills[i].first); button->eventMouseButtonClick += MyGUI::newDelegate(this, &TrainingWindow::onTrainingSelected); - button->setCaptionWithReplacing("#{" + ESM::Skill::sSkillNameIds[bestSkills[i].first] + "} - " + boost::lexical_cast(price)); + button->setCaptionWithReplacing("#{" + ESM::Skill::sSkillNameIds[skills[i].first] + "} - " + boost::lexical_cast(price)); button->setSize(button->getTextSize ().width+12, button->getSize().height); - ToolTips::createSkillToolTip (button, bestSkills[i].first); + ToolTips::createSkillToolTip (button, skills[i].first); } center(); @@ -107,7 +116,7 @@ namespace MWGui void TrainingWindow::onCancelButtonClicked (MyGUI::Widget *sender) { - MWBase::Environment::get().getWindowManager()->removeGuiMode (GM_Training); + exit(); } void TrainingWindow::onTrainingSelected (MyGUI::Widget *sender) @@ -123,6 +132,9 @@ namespace MWGui int price = pcStats.getSkill (skillId).getBase() * store.get().find("iTrainingMod")->getInt (); price = MWBase::Environment::get().getMechanicsManager()->getBarterOffer(mPtr,price,true); + if (price > player.getClass().getContainerStore(player).count(MWWorld::ContainerStore::sGoldId)) + return; + MWMechanics::NpcStats& npcStats = mPtr.getClass().getNpcStats (mPtr); if (npcStats.getSkill (skillId).getBase () <= pcStats.getSkill (skillId).getBase ()) { @@ -148,16 +160,19 @@ namespace MWGui // remove gold player.getClass().getContainerStore(player).remove(MWWorld::ContainerStore::sGoldId, price, player); + // add gold to NPC trading gold pool + npcStats.setGoldPool(npcStats.getGoldPool() + price); + // go back to game mode MWBase::Environment::get().getWindowManager()->removeGuiMode (GM_Training); - MWBase::Environment::get().getWindowManager()->removeGuiMode (GM_Dialogue); + MWBase::Environment::get().getDialogueManager()->goodbyeSelected(); // advance time MWBase::Environment::get().getWorld ()->advanceTime (2); MWBase::Environment::get().getMechanicsManager()->rest(false); MWBase::Environment::get().getMechanicsManager()->rest(false); - MWBase::Environment::get().getWorld ()->getFader()->fadeOut(0.25); + MWBase::Environment::get().getWindowManager()->fadeScreenOut(0.25); mFadeTimeRemaining = 0.5; } @@ -169,6 +184,6 @@ namespace MWGui mFadeTimeRemaining -= dt; if (mFadeTimeRemaining <= 0) - MWBase::Environment::get().getWorld ()->getFader()->fadeIn(0.25); + MWBase::Environment::get().getWindowManager()->fadeScreenIn(0.25); } } diff --git a/apps/openmw/mwgui/trainingwindow.hpp b/apps/openmw/mwgui/trainingwindow.hpp index 740115cdf..1fc902b20 100644 --- a/apps/openmw/mwgui/trainingwindow.hpp +++ b/apps/openmw/mwgui/trainingwindow.hpp @@ -14,6 +14,8 @@ namespace MWGui virtual void open(); + virtual void exit(); + void startTraining(MWWorld::Ptr actor); void onFrame(float dt); diff --git a/apps/openmw/mwgui/travelwindow.cpp b/apps/openmw/mwgui/travelwindow.cpp index 642e1a644..6a0c99b8a 100644 --- a/apps/openmw/mwgui/travelwindow.cpp +++ b/apps/openmw/mwgui/travelwindow.cpp @@ -4,15 +4,18 @@ #include -#include - #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/mechanicsmanager.hpp" +#include "../mwbase/soundmanager.hpp" +#include "../mwbase/dialoguemanager.hpp" + +#include "../mwmechanics/creaturestats.hpp" #include "../mwworld/class.hpp" #include "../mwworld/containerstore.hpp" +#include "../mwworld/actionteleport.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/cellstore.hpp" @@ -23,7 +26,6 @@ namespace MWGui TravelWindow::TravelWindow() : WindowBase("openmw_travel_window.layout") , mCurrentY(0) - , mLastPos(0) { setCoord(0, 0, 450, 300); @@ -45,7 +47,12 @@ namespace MWGui mSelect->getHeight()); } - void TravelWindow::addDestination(const std::string& travelId,ESM::Position pos,bool interior) + void TravelWindow::exit() + { + MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Travel); + } + + void TravelWindow::addDestination(const std::string& name,ESM::Position pos,bool interior) { int price = 0; @@ -80,13 +87,12 @@ namespace MWGui oss << price; toAdd->setUserString("price",oss.str()); - toAdd->setCaptionWithReplacing("#{sCell=" + travelId + "} - " + boost::lexical_cast(price)+"#{sgp}"); + toAdd->setCaptionWithReplacing("#{sCell=" + name + "} - " + boost::lexical_cast(price)+"#{sgp}"); toAdd->setSize(toAdd->getTextSize().width,sLineHeight); toAdd->eventMouseWheel += MyGUI::newDelegate(this, &TravelWindow::onMouseWheel); - toAdd->setUserString("Destination", travelId); + toAdd->setUserString("Destination", name); toAdd->setUserData(pos); toAdd->eventMouseButtonClick += MyGUI::newDelegate(this, &TravelWindow::onTravelButtonClick); - mDestinationsWidgetMap.insert(std::make_pair (toAdd, travelId)); } void TravelWindow::clearDestinations() @@ -95,7 +101,6 @@ namespace MWGui mCurrentY = 0; while (mDestinationsView->getChildCount()) MyGUI::Gui::getInstance().destroyWidget(mDestinationsView->getChildAt(0)); - mDestinationsWidgetMap.clear(); } void TravelWindow::startTravel(const MWWorld::Ptr& actor) @@ -113,14 +118,18 @@ namespace MWGui mPtr.get()->mBase->mTransport[i].mPos.pos[1],x,y); if (cellname == "") { - cellname = MWBase::Environment::get().getWorld()->getExterior(x,y)->getCell()->mName; + MWWorld::CellStore* cell = MWBase::Environment::get().getWorld()->getExterior(x,y); + cellname = MWBase::Environment::get().getWorld()->getCellName(cell); interior = false; } addDestination(cellname,mPtr.get()->mBase->mTransport[i].mPos,interior); } updateLabels(); + // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the scrollbar is hidden + mDestinationsView->setVisibleVScroll(false); mDestinationsView->setCanvasSize (MyGUI::IntSize(mDestinationsView->getWidth(), std::max(mDestinationsView->getHeight(), mCurrentY))); + mDestinationsView->setVisibleVScroll(true); } void TravelWindow::onTravelButtonClick(MyGUI::Widget* _sender) @@ -135,18 +144,21 @@ namespace MWGui if (playerGoldisExterior()) + // Interior cell -> mages guild transport + MWBase::Environment::get().getSoundManager()->playSound("mysticism cast", 1, 1); player.getClass().getContainerStore(player).remove(MWWorld::ContainerStore::sGoldId, price, player); - MWBase::Environment::get().getWorld ()->getFader ()->fadeOut(1); + // add gold to NPC trading gold pool + MWMechanics::CreatureStats& npcStats = mPtr.getClass().getCreatureStats(mPtr); + npcStats.setGoldPool(npcStats.getGoldPool() + price); + + MWBase::Environment::get().getWindowManager()->fadeScreenOut(1); ESM::Position pos = *_sender->getUserData(); std::string cellname = _sender->getUserString("Destination"); - int x,y; bool interior = _sender->getUserString("interior") == "y"; - MWBase::Environment::get().getWorld()->positionToIndex(pos.pos[0],pos.pos[1],x,y); - if(interior) - MWBase::Environment::get().getWorld()->changeToInteriorCell(cellname, pos); - else + if (!interior) { ESM::Position playerPos = player.getRefData().getPosition(); float d = Ogre::Vector3(pos.pos[0], pos.pos[1], 0).distance( @@ -157,20 +169,22 @@ namespace MWGui MWBase::Environment::get().getMechanicsManager ()->rest (true); } MWBase::Environment::get().getWorld()->advanceTime(hours); - - MWBase::Environment::get().getWorld()->changeToExteriorCell(pos); } - player.getClass().adjustPosition(player); MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Travel); - MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Dialogue); - MWBase::Environment::get().getWorld ()->getFader ()->fadeOut(0); - MWBase::Environment::get().getWorld ()->getFader ()->fadeIn(1); + MWBase::Environment::get().getDialogueManager()->goodbyeSelected(); + + // Teleports any followers, too. + MWWorld::ActionTeleport action(interior ? cellname : "", pos); + action.execute(player); + + MWBase::Environment::get().getWindowManager()->fadeScreenOut(0); + MWBase::Environment::get().getWindowManager()->fadeScreenIn(1); } void TravelWindow::onCancelButtonClicked(MyGUI::Widget* _sender) { - MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Travel); + exit(); } void TravelWindow::updateLabels() diff --git a/apps/openmw/mwgui/travelwindow.hpp b/apps/openmw/mwgui/travelwindow.hpp index f2a23b048..3f1949256 100644 --- a/apps/openmw/mwgui/travelwindow.hpp +++ b/apps/openmw/mwgui/travelwindow.hpp @@ -22,6 +22,8 @@ namespace MWGui public: TravelWindow(); + virtual void exit(); + void startTravel(const MWWorld::Ptr& actor); protected: @@ -32,14 +34,12 @@ namespace MWGui MyGUI::ScrollView* mDestinationsView; - std::map mDestinationsWidgetMap; - void onCancelButtonClicked(MyGUI::Widget* _sender); void onTravelButtonClick(MyGUI::Widget* _sender); void onMouseWheel(MyGUI::Widget* _sender, int _rel); - void addDestination(const std::string& destinationID,ESM::Position pos,bool interior); + void addDestination(const std::string& name, ESM::Position pos, bool interior); void clearDestinations(); - int mLastPos,mCurrentY; + int mCurrentY; static const int sLineHeight; diff --git a/apps/openmw/mwgui/videowidget.cpp b/apps/openmw/mwgui/videowidget.cpp index 566c7cadb..f8054925b 100644 --- a/apps/openmw/mwgui/videowidget.cpp +++ b/apps/openmw/mwgui/videowidget.cpp @@ -1,20 +1,18 @@ #include "videowidget.hpp" +#include "../mwsound/movieaudiofactory.hpp" + namespace MWGui { VideoWidget::VideoWidget() - : mAllowSkipping(true) { - eventKeyButtonPressed += MyGUI::newDelegate(this, &VideoWidget::onKeyPressed); - setNeedKeyFocus(true); } -void VideoWidget::playVideo(const std::string &video, bool allowSkipping) +void VideoWidget::playVideo(const std::string &video) { - mAllowSkipping = allowSkipping; - + mPlayer.setAudioFactory(new MWSound::MovieAudioFactory()); mPlayer.playVideo(video); setImageTexture(mPlayer.getTextureName()); @@ -30,16 +28,19 @@ int VideoWidget::getVideoHeight() return mPlayer.getVideoHeight(); } -void VideoWidget::onKeyPressed(MyGUI::Widget *_sender, MyGUI::KeyCode _key, MyGUI::Char _char) +bool VideoWidget::update() +{ + return mPlayer.update(); +} + +void VideoWidget::stop() { - if (_key == MyGUI::KeyCode::Escape && mAllowSkipping) - mPlayer.stopVideo(); + mPlayer.close(); } -bool VideoWidget::update() +bool VideoWidget::hasAudioStream() { - mPlayer.update(); - return mPlayer.isPlaying(); + return mPlayer.hasAudioStream(); } } diff --git a/apps/openmw/mwgui/videowidget.hpp b/apps/openmw/mwgui/videowidget.hpp index 16a71d367..79e1c26bb 100644 --- a/apps/openmw/mwgui/videowidget.hpp +++ b/apps/openmw/mwgui/videowidget.hpp @@ -3,13 +3,13 @@ #include -#include "../mwrender/videoplayer.hpp" +#include namespace MWGui { /** - * Widget that plays a video. Can be skipped by pressing Esc. + * Widget that plays a video. */ class VideoWidget : public MyGUI::ImageBox { @@ -18,7 +18,7 @@ namespace MWGui VideoWidget(); - void playVideo (const std::string& video, bool allowSkipping); + void playVideo (const std::string& video); int getVideoWidth(); int getVideoHeight(); @@ -26,12 +26,14 @@ namespace MWGui /// @return Is the video still playing? bool update(); - private: - bool mAllowSkipping; + /// Return true if a video is currently playing and it has an audio stream. + bool hasAudioStream(); - MWRender::VideoPlayer mPlayer; + /// Stop video and free resources (done automatically on destruction) + void stop(); - void onKeyPressed(MyGUI::Widget *_sender, MyGUI::KeyCode _key, MyGUI::Char _char); + private: + Video::VideoPlayer mPlayer; }; } diff --git a/apps/openmw/mwgui/waitdialog.cpp b/apps/openmw/mwgui/waitdialog.cpp index 12bf97f7c..f95ec5675 100644 --- a/apps/openmw/mwgui/waitdialog.cpp +++ b/apps/openmw/mwgui/waitdialog.cpp @@ -2,7 +2,7 @@ #include -#include +#include #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" @@ -69,6 +69,12 @@ namespace MWGui mProgressBar.setVisible (false); } + void WaitDialog::exit() + { + if(!mProgressBar.isVisible()) //Only exit if not currently waiting + MWBase::Environment::get().getWindowManager()->popGuiMode(); + } + void WaitDialog::open() { if (!MWBase::Environment::get().getWindowManager ()->getRestEnabled ()) @@ -122,7 +128,7 @@ namespace MWGui MWBase::Environment::get().getStateManager()->quickSave("Autosave"); MWBase::World* world = MWBase::Environment::get().getWorld(); - world->getFader ()->fadeOut(0.2); + MWBase::Environment::get().getWindowManager()->fadeScreenOut(0.2); setVisible(false); mProgressBar.setVisible (true); @@ -160,7 +166,7 @@ namespace MWGui void WaitDialog::onCancelButtonClicked(MyGUI::Widget* sender) { - MWBase::Environment::get().getWindowManager()->popGuiMode (); + exit(); } void WaitDialog::onHourSliderChangedPosition(MyGUI::ScrollBar* sender, size_t position) @@ -173,8 +179,7 @@ namespace MWGui { MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); MWMechanics::CreatureStats& stats = player.getClass().getCreatureStats(player); - bool full = (stats.getFatigue().getCurrent() >= stats.getFatigue().getModified()) - && (stats.getHealth().getCurrent() >= stats.getHealth().getModified()) + bool full = (stats.getHealth().getCurrent() >= stats.getHealth().getModified()) && (stats.getMagicka().getCurrent() >= stats.getMagicka().getModified()); MWMechanics::NpcStats& npcstats = player.getClass().getNpcStats(player); bool werewolf = npcstats.isWerewolf(); @@ -187,7 +192,10 @@ namespace MWGui mSleeping = canRest; - dynamic_cast(mMainWidget)->notifyChildrenSizeChanged(); + Gui::Box* box = dynamic_cast(mMainWidget); + if (box == NULL) + throw std::runtime_error("main widget must be a box"); + box->notifyChildrenSizeChanged(); center(); } @@ -237,7 +245,7 @@ namespace MWGui void WaitDialog::stopWaiting () { - MWBase::Environment::get().getWorld ()->getFader ()->fadeIn(0.2); + MWBase::Environment::get().getWindowManager()->fadeScreenIn(0.2); mProgressBar.setVisible (false); MWBase::Environment::get().getWindowManager()->removeGuiMode (GM_Rest); MWBase::Environment::get().getWindowManager()->removeGuiMode (GM_RestBed); diff --git a/apps/openmw/mwgui/waitdialog.hpp b/apps/openmw/mwgui/waitdialog.hpp index 5a66c3370..1cf05bb06 100644 --- a/apps/openmw/mwgui/waitdialog.hpp +++ b/apps/openmw/mwgui/waitdialog.hpp @@ -28,6 +28,8 @@ namespace MWGui virtual void open(); + virtual void exit(); + void onFrame(float dt); void bedActivated() { setCanRest(true); } diff --git a/apps/openmw/mwgui/widgets.cpp b/apps/openmw/mwgui/widgets.cpp index b30cf2bae..d7bc2c96e 100644 --- a/apps/openmw/mwgui/widgets.cpp +++ b/apps/openmw/mwgui/widgets.cpp @@ -5,6 +5,8 @@ #include #include +#include + #include #include #include @@ -20,21 +22,6 @@ namespace MWGui { namespace Widgets { - - /* Helper functions */ - - /* - * Fixes the filename of a texture path to use the correct .dds extension. - * This is needed on some ESM entries which point to a .tga file instead. - */ - void fixTexturePath(std::string &path) - { - int offset = path.rfind("."); - if (offset < 0) - return; - path.replace(offset, path.length() - offset, ".dds"); - } - /* MWSkill */ MWSkill::MWSkill() @@ -72,18 +59,18 @@ namespace MWGui { if (mSkillId == ESM::Skill::Length) { - static_cast(mSkillNameWidget)->setCaption(""); + mSkillNameWidget->setCaption(""); } else { const std::string &name = MWBase::Environment::get().getWindowManager()->getGameSettingString(ESM::Skill::sSkillNameIds[mSkillId], ""); - static_cast(mSkillNameWidget)->setCaption(name); + mSkillNameWidget->setCaption(name); } } if (mSkillValueWidget) { SkillValue::Type modified = mValue.getModified(), base = mValue.getBase(); - static_cast(mSkillValueWidget)->setCaption(boost::lexical_cast(modified)); + mSkillValueWidget->setCaption(boost::lexical_cast(modified)); if (modified > base) mSkillValueWidget->_setWidgetState("increased"); else if (modified < base) @@ -158,7 +145,7 @@ namespace MWGui { if (mId < 0 || mId >= 8) { - static_cast(mAttributeNameWidget)->setCaption(""); + mAttributeNameWidget->setCaption(""); } else { @@ -173,13 +160,13 @@ namespace MWGui "sAttributeLuck" }; const std::string &name = MWBase::Environment::get().getWindowManager()->getGameSettingString(attributes[mId], ""); - static_cast(mAttributeNameWidget)->setCaption(name); + mAttributeNameWidget->setCaption(name); } } if (mAttributeValueWidget) { int modified = mValue.getModified(), base = mValue.getBase(); - static_cast(mAttributeValueWidget)->setCaption(boost::lexical_cast(modified)); + mAttributeValueWidget->setCaption(boost::lexical_cast(modified)); if (modified > base) mAttributeValueWidget->_setWidgetState("increased"); else if (modified < base) @@ -238,11 +225,10 @@ namespace MWGui const ESM::Spell *spell = store.get().search(mId); MYGUI_ASSERT(spell, "spell with id '" << mId << "' not found"); - MWSpellEffectPtr effect = NULL; std::vector::const_iterator end = spell->mEffects.mList.end(); for (std::vector::const_iterator it = spell->mEffects.mList.begin(); it != end; ++it) { - effect = creator->createWidget("MW_EffectImage", coord, MyGUI::Align::Default); + MWSpellEffectPtr effect = creator->createWidget("MW_EffectImage", coord, MyGUI::Align::Default); SpellEffectParams params; params.mEffectID = it->mEffectID; params.mSkill = it->mSkill; @@ -269,9 +255,9 @@ namespace MWGui const ESM::Spell *spell = store.get().search(mId); if (spell) - static_cast(mSpellNameWidget)->setCaption(spell->mName); + mSpellNameWidget->setCaption(spell->mName); else - static_cast(mSpellNameWidget)->setCaption(""); + mSpellNameWidget->setCaption(""); } } @@ -323,7 +309,7 @@ namespace MWGui // ... then adjust the size for all widgets for (std::vector::iterator it = effects.begin(); it != effects.end(); ++it) { - effect = static_cast(*it); + effect = (*it)->castType(); bool needcenter = center && (maxwidth > effect->getRequestedWidth()); int diff = maxwidth - effect->getRequestedWidth(); if (needcenter) @@ -419,11 +405,11 @@ namespace MWGui std::string effectIDStr = ESM::MagicEffect::effectIdToString(mEffectParams.mEffectID); std::string spellLine = MWBase::Environment::get().getWindowManager()->getGameSettingString(effectIDStr, ""); - if (magicEffect->mData.mFlags & ESM::MagicEffect::TargetSkill) + if (magicEffect->mData.mFlags & ESM::MagicEffect::TargetSkill && mEffectParams.mSkill != -1) { spellLine += " " + MWBase::Environment::get().getWindowManager()->getGameSettingString(ESM::Skill::sSkillNameIds[mEffectParams.mSkill], ""); } - if (magicEffect->mData.mFlags & ESM::MagicEffect::TargetAttribute) + if (magicEffect->mData.mFlags & ESM::MagicEffect::TargetAttribute && mEffectParams.mAttribute != -1) { spellLine += " " + MWBase::Environment::get().getWindowManager()->getGameSettingString(ESM::Attribute::sGmstAttributeIds[mEffectParams.mAttribute], ""); } @@ -483,12 +469,10 @@ namespace MWGui } } - static_cast(mTextWidget)->setCaptionWithReplacing(spellLine); + mTextWidget->setCaptionWithReplacing(spellLine); mRequestedWidth = mTextWidget->getTextSize().width + 24; - std::string path = std::string("icons\\") + magicEffect->mIcon; - fixTexturePath(path); - mImageWidget->setImageTexture(path); + mImageWidget->setImageTexture(Misc::ResourceHelpers::correctIconPath(magicEffect->mIcon)); } MWSpellEffect::~MWSpellEffect() @@ -530,13 +514,13 @@ namespace MWGui { std::stringstream out; out << mValue << "/" << mMax; - static_cast(mBarTextWidget)->setCaption(out.str().c_str()); + mBarTextWidget->setCaption(out.str().c_str()); } } void MWDynamicStat::setTitle(const std::string& text) { if (mTextWidget) - static_cast(mTextWidget)->setCaption(text); + mTextWidget->setCaption(text); } MWDynamicStat::~MWDynamicStat() @@ -552,398 +536,6 @@ namespace MWGui assignWidget(mBarTextWidget, "BarText"); } - - - - // --------------------------------------------------------------------------------------------------------------------- - - void AutoSizedWidget::notifySizeChange (MyGUI::Widget* w) - { - if (w->getParent () != 0) - { - Box* b = dynamic_cast(w->getParent()); - if (b) - b->notifyChildrenSizeChanged (); - else - { - if (mExpandDirection == MyGUI::Align::Left) - { - int hdiff = getRequestedSize ().width - w->getSize().width; - w->setPosition(w->getPosition() - MyGUI::IntPoint(hdiff, 0)); - } - w->setSize(getRequestedSize ()); - } - } - } - - - MyGUI::IntSize AutoSizedTextBox::getRequestedSize() - { - return getTextSize(); - } - - void AutoSizedTextBox::setCaption(const MyGUI::UString& _value) - { - TextBox::setCaption(_value); - - notifySizeChange (this); - } - - void AutoSizedTextBox::setPropertyOverride(const std::string& _key, const std::string& _value) - { - if (_key == "ExpandDirection") - { - mExpandDirection = MyGUI::Align::parse (_value); - } - else - { - TextBox::setPropertyOverride (_key, _value); - } - } - - MyGUI::IntSize AutoSizedEditBox::getRequestedSize() - { - if (getAlign().isHStretch()) - throw std::runtime_error("AutoSizedEditBox can't have HStretch align (" + getName() + ")"); - return MyGUI::IntSize(getSize().width, getTextSize().height); - } - - void AutoSizedEditBox::setCaption(const MyGUI::UString& _value) - { - EditBox::setCaption(_value); - - notifySizeChange (this); - } - - void AutoSizedEditBox::setPropertyOverride(const std::string& _key, const std::string& _value) - { - if (_key == "ExpandDirection") - { - mExpandDirection = MyGUI::Align::parse (_value); - } - else - { - EditBox::setPropertyOverride (_key, _value); - } - } - - - MyGUI::IntSize AutoSizedButton::getRequestedSize() - { - MyGUI::IntSize size = getTextSize() + MyGUI::IntSize(24,0); - size.height = std::max(24, size.height); - return size; - } - - void AutoSizedButton::setCaption(const MyGUI::UString& _value) - { - Button::setCaption(_value); - - notifySizeChange (this); - } - - void AutoSizedButton::setPropertyOverride(const std::string& _key, const std::string& _value) - { - if (_key == "ExpandDirection") - { - mExpandDirection = MyGUI::Align::parse (_value); - } - else - { - Button::setPropertyOverride (_key, _value); - } - } - - Box::Box() - : mSpacing(4) - , mPadding(0) - , mAutoResize(false) - { - - } - - void Box::notifyChildrenSizeChanged () - { - align(); - } - - void Box::_setPropertyImpl(const std::string& _key, const std::string& _value) - { - if (_key == "Spacing") - mSpacing = MyGUI::utility::parseValue(_value); - else if (_key == "Padding") - mPadding = MyGUI::utility::parseValue(_value); - else if (_key == "AutoResize") - mAutoResize = MyGUI::utility::parseValue(_value); - } - - void HBox::align () - { - unsigned int count = getChildCount (); - size_t h_stretched_count = 0; - int total_width = 0; - int total_height = 0; - std::vector< std::pair > sizes; - sizes.resize(count); - - for (unsigned int i = 0; i < count; ++i) - { - MyGUI::Widget* w = getChildAt(i); - bool hstretch = w->getUserString ("HStretch") == "true"; - bool hidden = w->getUserString("Hidden") == "true"; - if (hidden) - continue; - h_stretched_count += hstretch; - AutoSizedWidget* aw = dynamic_cast(w); - if (aw) - { - sizes[i] = std::make_pair(aw->getRequestedSize (), hstretch); - total_width += aw->getRequestedSize ().width; - total_height = std::max(total_height, aw->getRequestedSize ().height); - } - else - { - sizes[i] = std::make_pair(w->getSize(), hstretch); - total_width += w->getSize().width; - if (!(w->getUserString("VStretch") == "true")) - total_height = std::max(total_height, w->getSize().height); - } - - if (i != count-1) - total_width += mSpacing; - } - - if (mAutoResize && (total_width+mPadding*2 != getSize().width || total_height+mPadding*2 != getSize().height)) - { - setSize(MyGUI::IntSize(total_width+mPadding*2, total_height+mPadding*2)); - return; - } - - - int curX = 0; - for (unsigned int i = 0; i < count; ++i) - { - if (i == 0) - curX += mPadding; - - MyGUI::Widget* w = getChildAt(i); - - bool hidden = w->getUserString("Hidden") == "true"; - if (hidden) - continue; - - bool vstretch = w->getUserString ("VStretch") == "true"; - int max_height = getSize().height - mPadding*2; - int height = vstretch ? max_height : sizes[i].first.height; - - MyGUI::IntCoord widgetCoord; - widgetCoord.left = curX; - widgetCoord.top = mPadding + (getSize().height-mPadding*2 - height) / 2; - int width = sizes[i].second ? sizes[i].first.width + (getSize().width-mPadding*2 - total_width)/h_stretched_count - : sizes[i].first.width; - widgetCoord.width = width; - widgetCoord.height = height; - w->setCoord(widgetCoord); - curX += width; - - if (i != count-1) - curX += mSpacing; - } - } - - void HBox::setPropertyOverride(const std::string& _key, const std::string& _value) - { - Box::_setPropertyImpl (_key, _value); - } - - void HBox::setSize (const MyGUI::IntSize& _value) - { - MyGUI::Widget::setSize (_value); - align(); - } - - void HBox::setCoord (const MyGUI::IntCoord& _value) - { - MyGUI::Widget::setCoord (_value); - align(); - } - - void HBox::onWidgetCreated(MyGUI::Widget* _widget) - { - align(); - } - - MyGUI::IntSize HBox::getRequestedSize () - { - MyGUI::IntSize size(0,0); - for (unsigned int i = 0; i < getChildCount (); ++i) - { - bool hidden = getChildAt(i)->getUserString("Hidden") == "true"; - if (hidden) - continue; - - AutoSizedWidget* w = dynamic_cast(getChildAt(i)); - if (w) - { - MyGUI::IntSize requested = w->getRequestedSize (); - size.height = std::max(size.height, requested.height); - size.width = size.width + requested.width; - if (i != getChildCount()-1) - size.width += mSpacing; - } - else - { - MyGUI::IntSize requested = getChildAt(i)->getSize (); - size.height = std::max(size.height, requested.height); - - if (getChildAt(i)->getUserString("HStretch") != "true") - size.width = size.width + requested.width; - - if (i != getChildCount()-1) - size.width += mSpacing; - } - size.height += mPadding*2; - size.width += mPadding*2; - } - return size; - } - - - - - void VBox::align () - { - unsigned int count = getChildCount (); - size_t v_stretched_count = 0; - int total_height = 0; - int total_width = 0; - std::vector< std::pair > sizes; - sizes.resize(count); - for (unsigned int i = 0; i < count; ++i) - { - MyGUI::Widget* w = getChildAt(i); - - bool hidden = w->getUserString("Hidden") == "true"; - if (hidden) - continue; - - bool vstretch = w->getUserString ("VStretch") == "true"; - v_stretched_count += vstretch; - AutoSizedWidget* aw = dynamic_cast(w); - if (aw) - { - sizes[i] = std::make_pair(aw->getRequestedSize (), vstretch); - total_height += aw->getRequestedSize ().height; - total_width = std::max(total_width, aw->getRequestedSize ().width); - } - else - { - sizes[i] = std::make_pair(w->getSize(), vstretch); - total_height += w->getSize().height; - - if (!(w->getUserString("HStretch") == "true")) - total_width = std::max(total_width, w->getSize().width); - } - - if (i != count-1) - total_height += mSpacing; - } - - if (mAutoResize && (total_width+mPadding*2 != getSize().width || total_height+mPadding*2 != getSize().height)) - { - setSize(MyGUI::IntSize(total_width+mPadding*2, total_height+mPadding*2)); - return; - } - - - int curY = 0; - for (unsigned int i = 0; i < count; ++i) - { - if (i==0) - curY += mPadding; - - MyGUI::Widget* w = getChildAt(i); - - bool hidden = w->getUserString("Hidden") == "true"; - if (hidden) - continue; - - bool hstretch = w->getUserString ("HStretch") == "true"; - int maxWidth = getSize().width - mPadding*2; - int width = hstretch ? maxWidth : sizes[i].first.width; - - MyGUI::IntCoord widgetCoord; - widgetCoord.top = curY; - widgetCoord.left = mPadding + (getSize().width-mPadding*2 - width) / 2; - int height = sizes[i].second ? sizes[i].first.height + (getSize().height-mPadding*2 - total_height)/v_stretched_count - : sizes[i].first.height; - widgetCoord.height = height; - widgetCoord.width = width; - w->setCoord(widgetCoord); - curY += height; - - if (i != count-1) - curY += mSpacing; - } - } - - void VBox::setPropertyOverride(const std::string& _key, const std::string& _value) - { - Box::_setPropertyImpl (_key, _value); - } - - void VBox::setSize (const MyGUI::IntSize& _value) - { - MyGUI::Widget::setSize (_value); - align(); - } - - void VBox::setCoord (const MyGUI::IntCoord& _value) - { - MyGUI::Widget::setCoord (_value); - align(); - } - - MyGUI::IntSize VBox::getRequestedSize () - { - MyGUI::IntSize size(0,0); - for (unsigned int i = 0; i < getChildCount (); ++i) - { - bool hidden = getChildAt(i)->getUserString("Hidden") == "true"; - if (hidden) - continue; - - AutoSizedWidget* w = dynamic_cast(getChildAt(i)); - if (w) - { - MyGUI::IntSize requested = w->getRequestedSize (); - size.width = std::max(size.width, requested.width); - size.height = size.height + requested.height; - if (i != getChildCount()-1) - size.height += mSpacing; - } - else - { - MyGUI::IntSize requested = getChildAt(i)->getSize (); - size.width = std::max(size.width, requested.width); - - if (getChildAt(i)->getUserString("VStretch") != "true") - size.height = size.height + requested.height; - - if (i != getChildCount()-1) - size.height += mSpacing; - } - size.height += mPadding*2; - size.width += mPadding*2; - } - return size; - } - - void VBox::onWidgetCreated(MyGUI::Widget* _widget) - { - align(); - } - MWScrollBar::MWScrollBar() : mEnableRepeat(true) , mRepeatTriggerTime(0.5) @@ -972,22 +564,6 @@ namespace MWGui } } - void MWScrollBar::setEnableRepeat(bool enable) - { - mEnableRepeat = enable; - } - - bool MWScrollBar::getEnableRepeat() - { - return mEnableRepeat; - } - - void MWScrollBar::getRepeat(float &trigger, float &step) - { - trigger = mRepeatTriggerTime; - step = mRepeatStepTime; - } - void MWScrollBar::setRepeat(float trigger, float step) { mRepeatTriggerTime = trigger; @@ -1024,8 +600,8 @@ namespace MWGui void MWScrollBar::onDecreaseButtonPressed(MyGUI::Widget* _sender, int _left, int _top, MyGUI::MouseButton _id) { mIsIncreasing = false; - MyGUI::ControllerItem* item = MyGUI::ControllerManager::getInstance().createItem(MWGui::Controllers::ControllerRepeatClick::getClassTypeName()); - MWGui::Controllers::ControllerRepeatClick* controller = item->castType(); + MyGUI::ControllerItem* item = MyGUI::ControllerManager::getInstance().createItem(MWGui::Controllers::ControllerRepeatEvent::getClassTypeName()); + MWGui::Controllers::ControllerRepeatEvent* controller = item->castType(); controller->eventRepeatClick += newDelegate(this, &MWScrollBar::repeatClick); controller->setEnabled(mEnableRepeat); controller->setRepeat(mRepeatTriggerTime, mRepeatStepTime); @@ -1040,8 +616,8 @@ namespace MWGui void MWScrollBar::onIncreaseButtonPressed(MyGUI::Widget* _sender, int _left, int _top, MyGUI::MouseButton _id) { mIsIncreasing = true; - MyGUI::ControllerItem* item = MyGUI::ControllerManager::getInstance().createItem(MWGui::Controllers::ControllerRepeatClick::getClassTypeName()); - MWGui::Controllers::ControllerRepeatClick* controller = item->castType(); + MyGUI::ControllerItem* item = MyGUI::ControllerManager::getInstance().createItem(MWGui::Controllers::ControllerRepeatEvent::getClassTypeName()); + MWGui::Controllers::ControllerRepeatEvent* controller = item->castType(); controller->eventRepeatClick += newDelegate(this, &MWScrollBar::repeatClick); controller->setEnabled(mEnableRepeat); controller->setRepeat(mRepeatTriggerTime, mRepeatStepTime); diff --git a/apps/openmw/mwgui/widgets.hpp b/apps/openmw/mwgui/widgets.hpp index adc56f423..aae28a686 100644 --- a/apps/openmw/mwgui/widgets.hpp +++ b/apps/openmw/mwgui/widgets.hpp @@ -122,8 +122,8 @@ namespace MWGui ESM::Skill::SkillEnum mSkillId; SkillValue mValue; - MyGUI::Widget* mSkillNameWidget; - MyGUI::Widget* mSkillValueWidget; + MyGUI::TextBox* mSkillNameWidget; + MyGUI::TextBox* mSkillValueWidget; }; typedef MWSkill* MWSkillPtr; @@ -162,8 +162,8 @@ namespace MWGui int mId; AttributeValue mValue; - MyGUI::Widget* mAttributeNameWidget; - MyGUI::Widget* mAttributeValueWidget; + MyGUI::TextBox* mAttributeNameWidget; + MyGUI::TextBox* mAttributeValueWidget; }; typedef MWAttribute* MWAttributePtr; @@ -293,123 +293,12 @@ namespace MWGui int mValue, mMax; MyGUI::TextBox* mTextWidget; - MyGUI::ProgressPtr mBarWidget; + MyGUI::ProgressBar* mBarWidget; MyGUI::TextBox* mBarTextWidget; }; typedef MWDynamicStat* MWDynamicStatPtr; - - - - - // --------------------------------------------------------------------------------------------------------------------- - - - - class AutoSizedWidget - { - public: - virtual MyGUI::IntSize getRequestedSize() = 0; - - protected: - void notifySizeChange(MyGUI::Widget* w); - - MyGUI::Align mExpandDirection; - }; - - class AutoSizedTextBox : public AutoSizedWidget, public MyGUI::TextBox - { - MYGUI_RTTI_DERIVED( AutoSizedTextBox ) - - public: - virtual MyGUI::IntSize getRequestedSize(); - virtual void setCaption(const MyGUI::UString& _value); - - protected: - virtual void setPropertyOverride(const std::string& _key, const std::string& _value); - }; - - class AutoSizedEditBox : public AutoSizedWidget, public MyGUI::EditBox - { - MYGUI_RTTI_DERIVED( AutoSizedEditBox ) - - public: - virtual MyGUI::IntSize getRequestedSize(); - virtual void setCaption(const MyGUI::UString& _value); - - protected: - virtual void setPropertyOverride(const std::string& _key, const std::string& _value); - }; - - class AutoSizedButton : public AutoSizedWidget, public MyGUI::Button - { - MYGUI_RTTI_DERIVED( AutoSizedButton ) - - public: - virtual MyGUI::IntSize getRequestedSize(); - virtual void setCaption(const MyGUI::UString& _value); - - protected: - virtual void setPropertyOverride(const std::string& _key, const std::string& _value); - }; - - /** - * @brief A container widget that automatically sizes its children - * @note the box being an AutoSizedWidget as well allows to put boxes inside a box - */ - class Box : public AutoSizedWidget - { - public: - Box(); - - void notifyChildrenSizeChanged(); - - protected: - virtual void align() = 0; - - virtual void _setPropertyImpl(const std::string& _key, const std::string& _value); - - int mSpacing; // how much space to put between elements - - int mPadding; // outer padding - - bool mAutoResize; // auto resize the box so that it exactly fits all elements - }; - - class HBox : public Box, public MyGUI::Widget - { - MYGUI_RTTI_DERIVED( HBox ) - - public: - virtual void setSize (const MyGUI::IntSize &_value); - virtual void setCoord (const MyGUI::IntCoord &_value); - - protected: - virtual void align(); - virtual MyGUI::IntSize getRequestedSize(); - - virtual void setPropertyOverride(const std::string& _key, const std::string& _value); - - virtual void onWidgetCreated(MyGUI::Widget* _widget); - }; - - class VBox : public Box, public MyGUI::Widget - { - MYGUI_RTTI_DERIVED( VBox) - - public: - virtual void setSize (const MyGUI::IntSize &_value); - virtual void setCoord (const MyGUI::IntCoord &_value); - - protected: - virtual void align(); - virtual MyGUI::IntSize getRequestedSize(); - - virtual void setPropertyOverride(const std::string& _key, const std::string& _value); - - virtual void onWidgetCreated(MyGUI::Widget* _widget); - }; - + // Should be removed when upgrading to MyGUI 3.2.2 (current git), it has ScrollBar autorepeat support class MWScrollBar : public MyGUI::ScrollBar { MYGUI_RTTI_DERIVED(MWScrollBar) @@ -418,9 +307,6 @@ namespace MWGui MWScrollBar(); virtual ~MWScrollBar(); - void setEnableRepeat(bool enable); - bool getEnableRepeat(); - void getRepeat(float &trigger, float &step); void setRepeat(float trigger, float step); protected: diff --git a/apps/openmw/mwgui/windowbase.cpp b/apps/openmw/mwgui/windowbase.cpp index 87b26b814..9c12b04ef 100644 --- a/apps/openmw/mwgui/windowbase.cpp +++ b/apps/openmw/mwgui/windowbase.cpp @@ -2,6 +2,8 @@ #include "../mwbase/windowmanager.hpp" #include "container.hpp" +#include "../mwbase/environment.hpp" +#include "../mwgui/windowmanagerimp.hpp" using namespace MWGui; @@ -19,6 +21,23 @@ void WindowBase::setVisible(bool visible) open(); else if (wasVisible && !visible) close(); + + // This is needed as invisible widgets can retain key focus. + // Remove for MyGUI 3.2.2 + if (!visible) + { + MyGUI::Widget* keyFocus = MyGUI::InputManager::getInstance().getKeyFocusWidget(); + while (keyFocus != mMainWidget && keyFocus != NULL) + keyFocus = keyFocus->getParent(); + + if (keyFocus == mMainWidget) + MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(NULL); + } +} + +bool WindowBase::isVisible() +{ + return mMainWidget->getVisible(); } void WindowBase::center() @@ -45,16 +64,20 @@ WindowModal::WindowModal(const std::string& parLayout) void WindowModal::open() { MyGUI::InputManager::getInstance ().addWidgetModal (mMainWidget); + MWBase::Environment::get().getWindowManager()->addCurrentModal(this); //Set so we can escape it if needed } void WindowModal::close() { MyGUI::InputManager::getInstance ().removeWidgetModal (mMainWidget); + MWBase::Environment::get().getWindowManager()->removeCurrentModal(this); } NoDrop::NoDrop(DragAndDrop *drag, MyGUI::Widget *widget) : mDrag(drag), mWidget(widget), mTransparent(false) { + if (!mWidget) + throw std::runtime_error("NoDrop needs a non-NULL widget!"); } void NoDrop::onFrame(float dt) @@ -76,11 +99,16 @@ void NoDrop::onFrame(float dt) if (mTransparent) { mWidget->setNeedMouseFocus(false); // Allow click-through - mWidget->setAlpha(std::max(0.13f, mWidget->getAlpha() - dt*5)); + setAlpha(std::max(0.13f, mWidget->getAlpha() - dt*5)); } else { mWidget->setNeedMouseFocus(true); - mWidget->setAlpha(std::min(1.0f, mWidget->getAlpha() + dt*5)); + setAlpha(std::min(1.0f, mWidget->getAlpha() + dt*5)); } } + +void NoDrop::setAlpha(float alpha) +{ + mWidget->setAlpha(alpha); +} diff --git a/apps/openmw/mwgui/windowbase.hpp b/apps/openmw/mwgui/windowbase.hpp index 48de9ea87..bf74c8bf0 100644 --- a/apps/openmw/mwgui/windowbase.hpp +++ b/apps/openmw/mwgui/windowbase.hpp @@ -21,15 +21,17 @@ namespace MWGui // Events typedef MyGUI::delegates::CMultiDelegate1 EventHandle_WindowBase; + ///Unhides the window virtual void open() {} + ///Hides the window virtual void close () {} + ///Gracefully exits the window + virtual void exit() {} + ///Sets the visibility of the window virtual void setVisible(bool visible); + ///Returns the visibility state of the window + virtual bool isVisible(); void center(); - - /** Event : Dialog finished, OK button clicked.\n - signature : void method()\n - */ - EventHandle_WindowBase eventDone; }; @@ -42,6 +44,7 @@ namespace MWGui WindowModal(const std::string& parLayout); virtual void open(); virtual void close(); + virtual void exit() {} }; /// A window that cannot be the target of a drag&drop action. @@ -52,6 +55,7 @@ namespace MWGui NoDrop(DragAndDrop* drag, MyGUI::Widget* widget); void onFrame(float dt); + virtual void setAlpha(float alpha); private: MyGUI::Widget* mWidget; diff --git a/apps/openmw/mwgui/windowmanagerimp.cpp b/apps/openmw/mwgui/windowmanagerimp.cpp index a42dca79e..6ab8b94c5 100644 --- a/apps/openmw/mwgui/windowmanagerimp.cpp +++ b/apps/openmw/mwgui/windowmanagerimp.cpp @@ -16,6 +16,11 @@ #include +#include + +#include +#include + #include "../mwbase/inputmanager.hpp" #include "../mwbase/statemanager.hpp" @@ -23,6 +28,10 @@ #include "../mwworld/player.hpp" #include "../mwworld/cellstore.hpp" +#include "../mwmechanics/npcstats.hpp" + +#include "../mwsound/soundmanagerimp.hpp" + #include "console.hpp" #include "journalwindow.hpp" #include "journalviewmodel.hpp" @@ -59,9 +68,11 @@ #include "inventorywindow.hpp" #include "bookpage.hpp" #include "itemview.hpp" -#include "fontloader.hpp" #include "videowidget.hpp" #include "backgroundimage.hpp" +#include "itemwidget.hpp" +#include "screenfader.hpp" +#include "debugwindow.hpp" namespace MWGui { @@ -69,7 +80,7 @@ namespace MWGui WindowManager::WindowManager( const Compiler::Extensions& extensions, int fpsLevel, OEngine::Render::OgreRenderer *ogre, const std::string& logpath, const std::string& cacheDir, bool consoleOnlyScripts, - Translation::Storage& translationDataStorage, ToUTF8::FromType encoding) + Translation::Storage& translationDataStorage, ToUTF8::FromType encoding, bool exportFonts, const std::map& fallbackMap) : mConsoleOnlyScripts(consoleOnlyScripts) , mGuiManager(NULL) , mRendering(ogre) @@ -109,12 +120,20 @@ namespace MWGui , mCompanionWindow(NULL) , mVideoBackground(NULL) , mVideoWidget(NULL) + , mWerewolfFader(NULL) + , mBlindnessFader(NULL) + , mHitFader(NULL) + , mScreenFader(NULL) + , mDebugWindow(NULL) , mTranslationDataStorage (translationDataStorage) , mCharGen(NULL) , mInputBlocker(NULL) , mCrosshairEnabled(Settings::Manager::getBool ("crosshair", "HUD")) , mSubtitlesEnabled(Settings::Manager::getBool ("subtitles", "GUI")) + , mHitFaderEnabled(Settings::Manager::getBool ("hit fader", "GUI")) + , mWerewolfOverlayEnabled(Settings::Manager::getBool ("werewolf overlay", "GUI")) , mHudEnabled(true) + , mGuiEnabled(true) , mCursorVisible(true) , mPlayerName() , mPlayerRaceId() @@ -134,14 +153,17 @@ namespace MWGui , mFPS(0.0f) , mTriangleCount(0) , mBatchCount(0) + , mCurrentModals() + , mFallbackMap(fallbackMap) { // Set up the GUI system mGuiManager = new OEngine::GUI::MyGUIManager(mRendering->getWindow(), mRendering->getScene(), false, logpath); - mGui = mGuiManager->getGui(); + + MyGUI::LanguageManager::getInstance().eventRequestTag = MyGUI::newDelegate(this, &WindowManager::onRetrieveTag); // Load fonts - FontLoader fontLoader (encoding); - fontLoader.loadAllFonts(); + Gui::FontLoader fontLoader (encoding); + fontLoader.loadAllFonts(exportFonts); //Register own widgets with MyGUI MyGUI::FactoryManager::getInstance().registerFactory("Widget"); @@ -150,33 +172,22 @@ namespace MWGui MyGUI::FactoryManager::getInstance().registerFactory("Widget"); MyGUI::FactoryManager::getInstance().registerFactory("Widget"); MyGUI::FactoryManager::getInstance().registerFactory("Widget"); - MyGUI::FactoryManager::getInstance().registerFactory("Widget"); - MyGUI::FactoryManager::getInstance().registerFactory("Widget"); - MyGUI::FactoryManager::getInstance().registerFactory("Widget"); - MyGUI::FactoryManager::getInstance().registerFactory("Widget"); - MyGUI::FactoryManager::getInstance().registerFactory("Widget"); - MyGUI::FactoryManager::getInstance().registerFactory("Widget"); - MyGUI::FactoryManager::getInstance().registerFactory("Widget"); MyGUI::FactoryManager::getInstance().registerFactory("Widget"); MyGUI::FactoryManager::getInstance().registerFactory("Widget"); MyGUI::FactoryManager::getInstance().registerFactory("Widget"); MyGUI::FactoryManager::getInstance().registerFactory("Widget"); BookPage::registerMyGUIComponents (); ItemView::registerComponents(); + ItemWidget::registerComponents(); + Gui::registerAllWidgets(); - MyGUI::FactoryManager::getInstance().registerFactory("Controller"); + MyGUI::FactoryManager::getInstance().registerFactory("Controller"); + MyGUI::FactoryManager::getInstance().registerFactory("Controller"); MyGUI::FactoryManager::getInstance().registerFactory("Resource", "ResourceImageSetPointer"); MyGUI::ResourceManager::getInstance().load("core.xml"); - MyGUI::LanguageManager::getInstance().eventRequestTag = MyGUI::newDelegate(this, &WindowManager::onRetrieveTag); - - // Get size info from the Gui object - int w = MyGUI::RenderManager::getInstance().getViewSize().width; - int h = MyGUI::RenderManager::getInstance().getViewSize().height; - mLoadingScreen = new LoadingScreen(mRendering->getScene (), mRendering->getWindow ()); - mLoadingScreen->onResChange (w,h); //set up the hardware cursor manager mCursorManager = new SFO::SDLCursorManager(); @@ -186,7 +197,6 @@ namespace MWGui MyGUI::InputManager::getInstance().eventChangeKeyFocus += MyGUI::newDelegate(this, &WindowManager::onKeyFocusChanged); onCursorChange(MyGUI::PointerManager::getInstance().getDefaultPointer()); - //SDL_ShowCursor(false); mCursorManager->setEnabled(true); @@ -197,8 +207,19 @@ namespace MWGui MyGUI::Align::Default, "Overlay"); mVideoBackground->setImageTexture("black.png"); mVideoBackground->setVisible(false); + mVideoBackground->setNeedMouseFocus(true); + mVideoBackground->setNeedKeyFocus(true); mVideoWidget = mVideoBackground->createWidgetReal("ImageBox", 0,0,1,1, MyGUI::Align::Default); + mVideoWidget->setNeedMouseFocus(true); + mVideoWidget->setNeedKeyFocus(true); + + // Removes default MyGUI system clipboard implementation, which supports windows only + MyGUI::ClipboardManager::getInstance().eventClipboardChanged.clear(); + MyGUI::ClipboardManager::getInstance().eventClipboardRequested.clear(); + + MyGUI::ClipboardManager::getInstance().eventClipboardChanged += MyGUI::newDelegate(this, &WindowManager::onClipboardChanged); + MyGUI::ClipboardManager::getInstance().eventClipboardRequested += MyGUI::newDelegate(this, &WindowManager::onClipboardRequested); } void WindowManager::initUI() @@ -207,17 +228,11 @@ namespace MWGui int w = MyGUI::RenderManager::getInstance().getViewSize().width; int h = MyGUI::RenderManager::getInstance().getViewSize().height; - MyGUI::Widget* dragAndDropWidget = mGui->createWidgetT("Widget","",0,0,w,h,MyGUI::Align::Default,"DragAndDrop","DragAndDropWidget"); - dragAndDropWidget->setVisible(false); - mDragAndDrop = new DragAndDrop(); - mDragAndDrop->mIsOnDragAndDrop = false; - mDragAndDrop->mDraggedWidget = 0; - mDragAndDrop->mDragAndDropWidget = dragAndDropWidget; mRecharge = new Recharge(); mMenu = new MainMenu(w,h); - mMap = new MapWindow(mDragAndDrop, ""); + mMap = new MapWindow(mCustomMarkers, mDragAndDrop, ""); trackWindow(mMap, "map"); mStatsWindow = new StatsWindow(mDragAndDrop); trackWindow(mStatsWindow, "stats"); @@ -235,7 +250,7 @@ namespace MWGui trackWindow(mDialogueWindow, "dialogue"); mContainerWindow = new ContainerWindow(mDragAndDrop); trackWindow(mContainerWindow, "container"); - mHud = new HUD(w,h, mShowFPSLevel, mDragAndDrop); + mHud = new HUD(mCustomMarkers, mShowFPSLevel, mDragAndDrop); mToolTips = new ToolTips(); mScrollWindow = new ScrollWindow(); mBookWindow = new BookWindow(); @@ -258,7 +273,19 @@ namespace MWGui mCompanionWindow = new CompanionWindow(mDragAndDrop, mMessageBoxManager); trackWindow(mCompanionWindow, "companion"); - mInputBlocker = mGui->createWidget("",0,0,w,h,MyGUI::Align::Default,"Windows",""); + mWerewolfFader = new ScreenFader("textures\\werewolfoverlay.dds"); + mBlindnessFader = new ScreenFader("black.png"); + std::string hitFaderTexture = "textures\\bm_player_hit_01.dds"; + // fall back to player_hit_01.dds if bm_player_hit_01.dds is not available + // TODO: check if non-BM versions actually use player_hit_01.dds + if(!Ogre::ResourceGroupManager::getSingleton().resourceExistsInAnyGroup(hitFaderTexture)) + hitFaderTexture = "textures\\player_hit_01.dds"; + mHitFader = new ScreenFader(hitFaderTexture); + mScreenFader = new ScreenFader("black.png"); + + mDebugWindow = new DebugWindow(); + + mInputBlocker = MyGUI::Gui::getInstance().createWidget("",0,0,w,h,MyGUI::Align::Stretch,"Overlay"); mHud->setVisible(mHudEnabled); @@ -347,6 +374,11 @@ namespace MWGui delete mCursorManager; delete mRecharge; delete mCompanionWindow; + delete mHitFader; + delete mWerewolfFader; + delete mScreenFader; + delete mBlindnessFader; + delete mDebugWindow; cleanupGarbage(); @@ -411,7 +443,7 @@ namespace MWGui mRecharge->setVisible(false); mVideoBackground->setVisible(false); - mHud->setVisible(mHudEnabled); + mHud->setVisible(mHudEnabled && mGuiEnabled); bool gameMode = !isGuiMode(); @@ -421,6 +453,13 @@ namespace MWGui if (gameMode) setKeyFocusWidget (NULL); + if (!mGuiEnabled) + { + if (containsMode(GM_Console)) + mConsole->setVisible(true); + return; + } + // Icons of forced hidden windows are displayed setMinimapVisibility((mAllowed & GW_Map) && (!mMap->pinned() || (mForceHidden & GW_Map))); setWeaponVisibility((mAllowed & GW_Inventory) && (!mInventoryWindow->pinned() || (mForceHidden & GW_Inventory))); @@ -439,128 +478,131 @@ namespace MWGui return; } - GuiMode mode = mGuiModes.back(); - - switch(mode) { - case GM_QuickKeysMenu: - mQuickKeysMenu->setVisible (true); - break; - case GM_MainMenu: - mMenu->setVisible(true); - break; - case GM_Settings: - mSettingsWindow->setVisible(true); - break; - case GM_Console: - mConsole->setVisible(true); - break; - case GM_Scroll: - mScrollWindow->setVisible(true); - break; - case GM_Book: - mBookWindow->setVisible(true); - break; - case GM_Alchemy: - mAlchemyWindow->setVisible(true); - break; - case GM_Rest: - mWaitDialog->setVisible(true); - break; - case GM_RestBed: - mWaitDialog->setVisible(true); - mWaitDialog->bedActivated(); - break; - case GM_Levelup: - mLevelupDialog->setVisible(true); - break; - case GM_Name: - case GM_Race: - case GM_Class: - case GM_ClassPick: - case GM_ClassCreate: - case GM_Birth: - case GM_ClassGenerate: - case GM_Review: - mCharGen->spawnDialog(mode); - break; - case GM_Inventory: - { - // First, compute the effective set of windows to show. - // This is controlled both by what windows the - // user has opened/closed (the 'shown' variable) and by what - // windows we are allowed to show (the 'allowed' var.) - int eff = mShown & mAllowed & ~mForceHidden; - - // Show the windows we want - mMap ->setVisible(eff & GW_Map); - mStatsWindow ->setVisible(eff & GW_Stats); - mInventoryWindow->setVisible(eff & GW_Inventory); - mInventoryWindow->setGuiMode(mode); - mSpellWindow ->setVisible(eff & GW_Magic); - break; + if(mGuiModes.size() != 0) + { + GuiMode mode = mGuiModes.back(); + + switch(mode) { + case GM_QuickKeysMenu: + mQuickKeysMenu->setVisible (true); + break; + case GM_MainMenu: + mMenu->setVisible(true); + break; + case GM_Settings: + mSettingsWindow->setVisible(true); + break; + case GM_Console: + mConsole->setVisible(true); + break; + case GM_Scroll: + mScrollWindow->setVisible(true); + break; + case GM_Book: + mBookWindow->setVisible(true); + break; + case GM_Alchemy: + mAlchemyWindow->setVisible(true); + break; + case GM_Rest: + mWaitDialog->setVisible(true); + break; + case GM_RestBed: + mWaitDialog->setVisible(true); + mWaitDialog->bedActivated(); + break; + case GM_Levelup: + mLevelupDialog->setVisible(true); + break; + case GM_Name: + case GM_Race: + case GM_Class: + case GM_ClassPick: + case GM_ClassCreate: + case GM_Birth: + case GM_ClassGenerate: + case GM_Review: + mCharGen->spawnDialog(mode); + break; + case GM_Inventory: + { + // First, compute the effective set of windows to show. + // This is controlled both by what windows the + // user has opened/closed (the 'shown' variable) and by what + // windows we are allowed to show (the 'allowed' var.) + int eff = mShown & mAllowed & ~mForceHidden; + + // Show the windows we want + mMap ->setVisible(eff & GW_Map); + mStatsWindow ->setVisible(eff & GW_Stats); + mInventoryWindow->setVisible(eff & GW_Inventory); + mInventoryWindow->setGuiMode(mode); + mSpellWindow ->setVisible(eff & GW_Magic); + break; + } + case GM_Container: + mContainerWindow->setVisible(true); + mInventoryWindow->setVisible(true); + mInventoryWindow->setGuiMode(mode); + break; + case GM_Companion: + mCompanionWindow->setVisible(true); + mInventoryWindow->setVisible(true); + mInventoryWindow->setGuiMode(mode); + break; + case GM_Dialogue: + mDialogueWindow->setVisible(true); + break; + case GM_Barter: + mInventoryWindow->setVisible(true); + mInventoryWindow->setTrading(true); + mInventoryWindow->setGuiMode(mode); + mTradeWindow->setVisible(true); + break; + case GM_SpellBuying: + mSpellBuyingWindow->setVisible(true); + break; + case GM_Travel: + mTravelWindow->setVisible(true); + break; + case GM_SpellCreation: + mSpellCreationDialog->setVisible(true); + break; + case GM_Recharge: + mRecharge->setVisible(true); + break; + case GM_Enchanting: + mEnchantingDialog->setVisible(true); + break; + case GM_Training: + mTrainingWindow->setVisible(true); + break; + case GM_MerchantRepair: + mMerchantRepair->setVisible(true); + break; + case GM_Repair: + mRepair->setVisible(true); + break; + case GM_Journal: + mJournal->setVisible(true); + break; + case GM_LoadingWallpaper: + mHud->setVisible(false); + setCursorVisible(false); + break; + case GM_Loading: + // Show the pinned windows + mMap->setVisible(mMap->pinned() && !(mForceHidden & GW_Map)); + mStatsWindow->setVisible(mStatsWindow->pinned() && !(mForceHidden & GW_Stats)); + mInventoryWindow->setVisible(mInventoryWindow->pinned() && !(mForceHidden & GW_Inventory)); + mSpellWindow->setVisible(mSpellWindow->pinned() && !(mForceHidden & GW_Magic)); + + setCursorVisible(false); + break; + default: + // Unsupported mode, switch back to game + break; } - case GM_Container: - mContainerWindow->setVisible(true); - mInventoryWindow->setVisible(true); - mInventoryWindow->setGuiMode(mode); - break; - case GM_Companion: - mCompanionWindow->setVisible(true); - mInventoryWindow->setVisible(true); - mInventoryWindow->setGuiMode(mode); - break; - case GM_Dialogue: - mDialogueWindow->setVisible(true); - break; - case GM_Barter: - mInventoryWindow->setVisible(true); - mInventoryWindow->setTrading(true); - mInventoryWindow->setGuiMode(mode); - mTradeWindow->setVisible(true); - break; - case GM_SpellBuying: - mSpellBuyingWindow->setVisible(true); - break; - case GM_Travel: - mTravelWindow->setVisible(true); - break; - case GM_SpellCreation: - mSpellCreationDialog->setVisible(true); - break; - case GM_Recharge: - mRecharge->setVisible(true); - break; - case GM_Enchanting: - mEnchantingDialog->setVisible(true); - break; - case GM_Training: - mTrainingWindow->setVisible(true); - break; - case GM_MerchantRepair: - mMerchantRepair->setVisible(true); - break; - case GM_Repair: - mRepair->setVisible(true); - break; - case GM_Journal: - mJournal->setVisible(true); - break; - case GM_LoadingWallpaper: - mHud->setVisible(false); - setCursorVisible(false); - break; - case GM_Loading: - // Show the pinned windows - mMap->setVisible(mMap->pinned() && !(mForceHidden & GW_Map)); - mStatsWindow->setVisible(mStatsWindow->pinned() && !(mForceHidden & GW_Stats)); - mInventoryWindow->setVisible(mInventoryWindow->pinned() && !(mForceHidden & GW_Inventory)); - mSpellWindow->setVisible(mSpellWindow->pinned() && !(mForceHidden & GW_Magic)); - - setCursorVisible(false); - break; - default: - // Unsupported mode, switch back to game - break; } } @@ -666,6 +708,93 @@ namespace MWGui mGarbageDialogs.push_back(dialog); } + void WindowManager::exitCurrentGuiMode() { + switch(mGuiModes.back()) { + case GM_QuickKeysMenu: + mQuickKeysMenu->exit(); + break; + case GM_MainMenu: + removeGuiMode(GM_MainMenu); //Simple way to remove it + break; + case GM_Settings: + mSettingsWindow->exit(); + break; + case GM_Console: + mConsole->exit(); + break; + case GM_Scroll: + mScrollWindow->exit(); + break; + case GM_Book: + mBookWindow->exit(); + break; + case GM_Alchemy: + mAlchemyWindow->exit(); + break; + case GM_Rest: + mWaitDialog->exit(); + break; + case GM_RestBed: + mWaitDialog->exit(); + break; + case GM_Name: + case GM_Race: + case GM_Class: + case GM_ClassPick: + case GM_ClassCreate: + case GM_Birth: + case GM_ClassGenerate: + case GM_Review: + break; + case GM_Inventory: + removeGuiMode(GM_Inventory); //Simple way to remove it + break; + case GM_Container: + mContainerWindow->exit(); + break; + case GM_Companion: + mCompanionWindow->exit(); + break; + case GM_Dialogue: + mDialogueWindow->exit(); + break; + case GM_Barter: + mTradeWindow->exit(); + break; + case GM_SpellBuying: + mSpellBuyingWindow->exit(); + break; + case GM_Travel: + mTravelWindow->exit(); + break; + case GM_SpellCreation: + mSpellCreationDialog->exit(); + break; + case GM_Recharge: + mRecharge->exit(); + break; + case GM_Enchanting: + mEnchantingDialog->exit(); + break; + case GM_Training: + mTrainingWindow->exit(); + break; + case GM_MerchantRepair: + mMerchantRepair->exit(); + break; + case GM_Repair: + mRepair->exit(); + break; + case GM_Journal: + MWBase::Environment::get().getSoundManager()->playSound ("book close", 1.0, 1.0); + removeGuiMode(GM_Journal); //Simple way to remove it + break; + default: + // Unsupported mode, switch back to game + break; + } + } + void WindowManager::messageBox (const std::string& message, const std::vector& buttons, enum MWGui::ShowInDialogueMode showInDialogueMode) { if (buttons.empty()) { @@ -678,6 +807,7 @@ namespace MWGui } else { mMessageBoxManager->createInteractiveMessageBox(message, buttons); MWBase::Environment::get().getInputManager()->changeInputMode(isGuiMode()); + updateVisible(); } } @@ -713,6 +843,8 @@ namespace MWGui mToolTips->onFrame(frameDuration); + mMenu->update(frameDuration); + if (MWBase::Environment::get().getStateManager()->getState()== MWBase::StateManager::State_NoGame) return; @@ -736,7 +868,6 @@ namespace MWGui mHud->onFrame(frameDuration); mTrainingWindow->onFrame (frameDuration); - mTradeWindow->onFrame(frameDuration); mTrainingWindow->checkReferenceAvailable(); mDialogueWindow->checkReferenceAvailable(); @@ -748,6 +879,13 @@ namespace MWGui mCompanionWindow->checkReferenceAvailable(); mConsole->checkReferenceAvailable(); mCompanionWindow->onFrame(); + + mWerewolfFader->update(frameDuration); + mBlindnessFader->update(frameDuration); + mHitFader->update(frameDuration); + mScreenFader->update(frameDuration); + + mDebugWindow->onFrame(frameDuration); } void WindowManager::changeCell(MWWorld::CellStore* cell) @@ -763,9 +901,6 @@ namespace MWGui mMap->addVisitedLocation ("#{sCell=" + name + "}", cell->getCell()->getGridX (), cell->getCell()->getGridY ()); mMap->cellExplored (cell->getCell()->getGridX(), cell->getCell()->getGridY()); - - mMap->setCellPrefix("Cell"); - mHud->setCellPrefix("Cell"); } else { @@ -783,19 +918,26 @@ namespace MWGui void WindowManager::setActiveMap(int x, int y, bool interior) { + if (!interior) + { + mMap->setCellPrefix("Cell"); + mHud->setCellPrefix("Cell"); + } + mMap->setActiveCell(x,y, interior); mHud->setActiveCell(x,y, interior); } - void WindowManager::setPlayerPos(const float x, const float y) + void WindowManager::setPlayerPos(int cellX, int cellY, const float x, const float y) { - mMap->setPlayerPos(x,y); - mHud->setPlayerPos(x,y); + mMap->setPlayerPos(cellX, cellY, x, y); + mHud->setPlayerPos(cellX, cellY, x, y); } void WindowManager::setPlayerDir(const float x, const float y) { mMap->setPlayerDir(x,y); + mMap->setGlobalMapPlayerDir(x, y); mHud->setPlayerDir(x,y); } @@ -874,10 +1016,14 @@ namespace MWGui std::string tokenToFind = "sCell="; size_t tokenLength = tokenToFind.length(); - if (tag.substr(0, tokenLength) == tokenToFind) + if (tag.compare(0, tokenLength, tokenToFind) == 0) { _result = mTranslationDataStorage.translateCellName(tag.substr(tokenLength)); } + else if (Gui::replaceTag(tag, _result, mFallbackMap)) + { + return; + } else { const ESM::GameSetting *setting = @@ -909,7 +1055,6 @@ namespace MWGui { sizeVideo(x, y); mGuiManager->windowResized(); - mLoadingScreen->onResChange (x,y); if (!mHud) return; // UI not initialized yet @@ -923,7 +1068,6 @@ namespace MWGui it->first->setSize(size); } - mHud->onResChange(x, y); mConsole->onResChange(x, y); mMenu->onResChange(x, y); mSettingsWindow->center(); @@ -932,8 +1076,6 @@ namespace MWGui mBookWindow->center(); mQuickKeysMenu->center(); mSpellBuyingWindow->center(); - mDragAndDrop->mDragAndDropWidget->setSize(MyGUI::IntSize(x, y)); - mInputBlocker->setSize(MyGUI::IntSize(x,y)); } void WindowManager::pushGuiMode(GuiMode mode) @@ -1095,8 +1237,6 @@ namespace MWGui mBatchCount = batchCount; } - MyGUI::Gui* WindowManager::getGui() const { return mGui; } - MWGui::DialogueWindow* WindowManager::getDialogueWindow() { return mDialogueWindow; } MWGui::ContainerWindow* WindowManager::getContainerWindow() { return mContainerWindow; } MWGui::InventoryWindow* WindowManager::getInventoryWindow() { return mInventoryWindow; } @@ -1238,10 +1378,11 @@ namespace MWGui return mSubtitlesEnabled; } - void WindowManager::toggleHud () + bool WindowManager::toggleGui() { - mHudEnabled = !mHudEnabled; - mHud->setVisible (mHudEnabled); + mGuiEnabled = !mGuiEnabled; + updateVisible(); + return mGuiEnabled; } bool WindowManager::getRestEnabled() @@ -1327,8 +1468,16 @@ namespace MWGui void WindowManager::updatePlayer() { mInventoryWindow->updatePlayer(); + + const MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); + if (player.getClass().getNpcStats(player).isWerewolf()) + { + setWerewolfOverlay(true); + forceHide((GuiWindow)(MWGui::GW_Inventory | MWGui::GW_Magic)); + } } + // Remove this method for MyGUI 3.2.2 void WindowManager::setKeyFocusWidget(MyGUI::Widget *widget) { if (widget == NULL) @@ -1401,6 +1550,7 @@ namespace MWGui { mMap->clear(); mQuickKeysMenu->clear(); + mMessageBoxManager->clear(); mTrainingWindow->resetReference(); mDialogueWindow->resetReference(); @@ -1412,6 +1562,14 @@ namespace MWGui mCompanionWindow->resetReference(); mConsole->resetReference(); + mSelectedSpell.clear(); + + mCustomMarkers.clear(); + + mForceHidden = GW_None; + + setWerewolfOverlay(false); + mGuiModes.clear(); MWBase::Environment::get().getInputManager()->changeInputMode(false); updateVisible(); @@ -1431,6 +1589,14 @@ namespace MWGui writer.endRecord(ESM::REC_ASPL); progress.increaseProgress(); } + + for (std::vector::const_iterator it = mCustomMarkers.begin(); it != mCustomMarkers.end(); ++it) + { + writer.startRecord(ESM::REC_MARK); + (*it).save(writer); + writer.endRecord(ESM::REC_MARK); + progress.increaseProgress(); + } } void WindowManager::readRecord(ESM::ESMReader &reader, int32_t type) @@ -1444,12 +1610,19 @@ namespace MWGui reader.getSubNameIs("ID__"); mSelectedSpell = reader.getHString(); } + else if (type == ESM::REC_MARK) + { + CustomMarker marker; + marker.load(reader); + mCustomMarkers.addMarker(marker, false); + } } int WindowManager::countSavedGameRecords() const { return 1 // Global map + 1 // QuickKeysMenu + + mCustomMarkers.size() + (!mSelectedSpell.empty() ? 1 : 0); } @@ -1462,7 +1635,15 @@ namespace MWGui void WindowManager::playVideo(const std::string &name, bool allowSkipping) { - mVideoWidget->playVideo("video\\" + name, allowSkipping); + mVideoWidget->playVideo("video\\" + name); + + mVideoWidget->eventKeyButtonPressed.clear(); + mVideoBackground->eventKeyButtonPressed.clear(); + if (allowSkipping) + { + mVideoWidget->eventKeyButtonPressed += MyGUI::newDelegate(this, &WindowManager::onVideoKeyPressed); + mVideoBackground->eventKeyButtonPressed += MyGUI::newDelegate(this, &WindowManager::onVideoKeyPressed); + } // Turn off all rendering except for the GUI mRendering->getScene()->clearSpecialCaseRenderQueues(); @@ -1484,12 +1665,19 @@ namespace MWGui bool cursorWasVisible = mCursorVisible; setCursorVisible(false); - while (mVideoWidget->update()) + if (mVideoWidget->hasAudioStream()) + MWBase::Environment::get().getSoundManager()->pauseSounds( + MWBase::SoundManager::Play_TypeMask&(~MWBase::SoundManager::Play_TypeMovie)); + + while (mVideoWidget->update() && !MWBase::Environment::get().getStateManager()->hasQuitRequest()) { MWBase::Environment::get().getInputManager()->update(0, true, false); mRendering->getWindow()->update(); } + mVideoWidget->stop(); + + MWBase::Environment::get().getSoundManager()->resumeSounds(); setCursorVisible(cursorWasVisible); @@ -1513,4 +1701,122 @@ namespace MWGui mVideoWidget->setCoord(leftPadding, topPadding, screenWidth - leftPadding*2, screenHeight - topPadding*2); } + + WindowModal* WindowManager::getCurrentModal() const + { + if(!mCurrentModals.empty()) + return mCurrentModals.top(); + else + return NULL; + } + + void WindowManager::removeCurrentModal(WindowModal* input) + { + // Only remove the top if it matches the current pointer. A lot of things hide their visibility before showing it, + //so just popping the top would cause massive issues. + if(!mCurrentModals.empty()) + if(input == mCurrentModals.top()) + mCurrentModals.pop(); + } + + void WindowManager::onVideoKeyPressed(MyGUI::Widget *_sender, MyGUI::KeyCode _key, MyGUI::Char _char) + { + if (_key == MyGUI::KeyCode::Escape) + mVideoWidget->stop(); + } + + void WindowManager::pinWindow(GuiWindow window) + { + switch (window) + { + case GW_Inventory: + mInventoryWindow->setPinned(true); + break; + case GW_Map: + mMap->setPinned(true); + break; + case GW_Magic: + mSpellWindow->setPinned(true); + break; + case GW_Stats: + mStatsWindow->setPinned(true); + break; + default: + break; + } + + updateVisible(); + } + + void WindowManager::fadeScreenIn(const float time) + { + mScreenFader->clearQueue(); + mScreenFader->fadeOut(time); + } + + void WindowManager::fadeScreenOut(const float time) + { + mScreenFader->clearQueue(); + mScreenFader->fadeIn(time); + } + + void WindowManager::fadeScreenTo(const int percent, const float time) + { + mScreenFader->clearQueue(); + mScreenFader->fadeTo(percent, time); + } + + void WindowManager::setBlindness(const int percent) + { + mBlindnessFader->notifyAlphaChanged(percent / 100.f); + } + + void WindowManager::activateHitOverlay(bool interrupt) + { + if (!mHitFaderEnabled) + return; + + if (!interrupt && !mHitFader->isEmpty()) + return; + + mHitFader->clearQueue(); + mHitFader->fadeTo(100, 0.0f); + mHitFader->fadeTo(0, 0.5f); + } + + void WindowManager::setWerewolfOverlay(bool set) + { + if (!mWerewolfOverlayEnabled) + return; + + mWerewolfFader->notifyAlphaChanged(set ? 1.0f : 0.0f); + } + + void WindowManager::onClipboardChanged(const std::string &_type, const std::string &_data) + { + if (_type == "Text") + SDL_SetClipboardText(MyGUI::TextIterator::getOnlyText(MyGUI::UString(_data)).asUTF8().c_str()); + } + + void WindowManager::onClipboardRequested(const std::string &_type, std::string &_data) + { + if (_type != "Text") + return; + char* text=0; + text = SDL_GetClipboardText(); + if (text) + { + // MyGUI's clipboard might still have color information, to retain that information, only set the new text + // if it actually changed (clipboard inserted by an external application) + if (MyGUI::TextIterator::getOnlyText(_data) != text) + _data = text; + } + SDL_free(text); + } + + void WindowManager::toggleDebugWindow() + { + mDebugWindow->setVisible(!mDebugWindow->isVisible()); + } + } diff --git a/apps/openmw/mwgui/windowmanagerimp.hpp b/apps/openmw/mwgui/windowmanagerimp.hpp index c98c32c52..aa5bd0fc9 100644 --- a/apps/openmw/mwgui/windowmanagerimp.hpp +++ b/apps/openmw/mwgui/windowmanagerimp.hpp @@ -12,6 +12,11 @@ #include "../mwbase/windowmanager.hpp" +#include "mapwindow.hpp" + +#include +#include + namespace MyGUI { class Gui; @@ -74,7 +79,6 @@ namespace MWGui class SpellCreationDialog; class EnchantingDialog; class TrainingWindow; - class Cursor; class SpellIcons; class MerchantRepair; class Repair; @@ -82,6 +86,9 @@ namespace MWGui class Recharge; class CompanionWindow; class VideoWidget; + class WindowModal; + class ScreenFader; + class DebugWindow; class WindowManager : public MWBase::WindowManager { @@ -92,7 +99,7 @@ namespace MWGui WindowManager(const Compiler::Extensions& extensions, int fpsLevel, OEngine::Render::OgreRenderer *mOgre, const std::string& logpath, const std::string& cacheDir, bool consoleOnlyScripts, - Translation::Storage& translationDataStorage, ToUTF8::FromType encoding); + Translation::Storage& translationDataStorage, ToUTF8::FromType encoding, bool exportFonts, const std::map& fallbackMap); virtual ~WindowManager(); void initUI(); @@ -132,10 +139,10 @@ namespace MWGui virtual void forceHide(MWGui::GuiWindow wnd); virtual void unsetForceHide(MWGui::GuiWindow wnd); - // Disallow all inventory mode windows + /// Disallow all inventory mode windows virtual void disallowAll(); - // Allow one or more windows + /// Allow one or more windows virtual void allow(GuiWindow wnd); virtual bool isAllowed(GuiWindow wnd) const; @@ -154,8 +161,6 @@ namespace MWGui virtual MWGui::SpellWindow* getSpellWindow(); virtual MWGui::Console* getConsole(); - virtual MyGUI::Gui* getGui() const; - virtual void wmUpdateFps(float fps, unsigned int triangleCount, unsigned int batchCount); ///< Set value for the given ID. @@ -177,7 +182,7 @@ namespace MWGui virtual void updateSkillArea(); ///< update display of skills, factions, birth sign, reputation and bounty virtual void changeCell(MWWorld::CellStore* cell); ///< change the active cell - virtual void setPlayerPos(const float x, const float y); ///< set player position in map space + virtual void setPlayerPos(int cellX, int cellY, const float x, const float y); ///< set player position in map space virtual void setPlayerDir(const float x, const float y); ///< set player view direction in map space virtual void setFocusObject(const MWWorld::Ptr& focus); @@ -217,7 +222,9 @@ namespace MWGui virtual void showCrosshair(bool show); virtual bool getSubtitlesEnabled(); - virtual void toggleHud(); + + /// Turn visibility of *all* GUI elements on or off (HUD and all windows, except the console) + virtual bool toggleGui(); virtual void disallowMouse(); virtual void allowMouse(); @@ -225,7 +232,11 @@ namespace MWGui virtual void addVisitedLocation(const std::string& name, int x, int y); - virtual void removeDialog(OEngine::GUI::Layout* dialog); ///< Hides dialog and schedules dialog to be deleted. + ///Hides dialog and schedules dialog to be deleted. + virtual void removeDialog(OEngine::GUI::Layout* dialog); + + ///Gracefully attempts to exit the topmost GUI mode + virtual void exitCurrentGuiMode(); virtual void messageBox (const std::string& message, const std::vector& buttons = std::vector(), enum MWGui::ShowInDialogueMode showInDialogueMode = MWGui::ShowInDialogueMode_IfPossible); virtual void staticMessageBox(const std::string& message); @@ -298,6 +309,35 @@ namespace MWGui /// Does the current stack of GUI-windows permit saving? virtual bool isSavingAllowed() const; + /// Returns the current Modal + /** Used to send exit command to active Modal when Esc is pressed **/ + virtual WindowModal* getCurrentModal() const; + + /// Sets the current Modal + /** Used to send exit command to active Modal when Esc is pressed **/ + virtual void addCurrentModal(WindowModal* input) {mCurrentModals.push(input);} + + /// Removes the top Modal + /** Used when one Modal adds another Modal + \param input Pointer to the current modal, to ensure proper modal is removed **/ + virtual void removeCurrentModal(WindowModal* input); + + virtual void pinWindow (MWGui::GuiWindow window); + + /// Fade the screen in, over \a time seconds + virtual void fadeScreenIn(const float time); + /// Fade the screen out to black, over \a time seconds + virtual void fadeScreenOut(const float time); + /// Fade the screen to a specified percentage of black, over \a time seconds + virtual void fadeScreenTo(const int percent, const float time); + /// Darken the screen to a specified percentage + virtual void setBlindness(const int percent); + + virtual void activateHitOverlay(bool interrupt); + virtual void setWerewolfOverlay(bool set); + + virtual void toggleDebugWindow(); + private: bool mConsoleOnlyScripts; @@ -307,6 +347,11 @@ namespace MWGui std::string mSelectedSpell; + std::stack mCurrentModals; + + // Markers placed manually by the player. Must be shared between both map views (the HUD map and the map window). + CustomMarkerCollection mCustomMarkers; + OEngine::GUI::MyGUIManager *mGuiManager; OEngine::Render::OgreRenderer *mRendering; HUD *mHud; @@ -345,9 +390,13 @@ namespace MWGui CompanionWindow* mCompanionWindow; MyGUI::ImageBox* mVideoBackground; VideoWidget* mVideoWidget; + ScreenFader* mWerewolfFader; + ScreenFader* mBlindnessFader; + ScreenFader* mHitFader; + ScreenFader* mScreenFader; + DebugWindow* mDebugWindow; Translation::Storage& mTranslationDataStorage; - Cursor* mSoftwareCursor; CharacterCreation* mCharGen; @@ -355,7 +404,10 @@ namespace MWGui bool mCrosshairEnabled; bool mSubtitlesEnabled; + bool mHitFaderEnabled; + bool mWerewolfOverlayEnabled; bool mHudEnabled; + bool mGuiEnabled; bool mCursorVisible; void setCursorVisible(bool visible); @@ -395,16 +447,30 @@ namespace MWGui unsigned int mTriangleCount; unsigned int mBatchCount; + std::map mFallbackMap; + /** - * Called when MyGUI tries to retrieve a tag. This usually corresponds to a GMST string, - * so this method will retrieve the GMST with the name \a _tag and place the result in \a _result + * Called when MyGUI tries to retrieve a tag's value. Tags must be denoted in #{tag} notation and will be replaced upon setting a user visible text/property. + * Supported syntax: + * #{GMSTName}: retrieves String value of the GMST called GMSTName + * #{sCell=CellID}: retrieves translated name of the given CellID (used only by some Morrowind localisations, in others cell ID is == cell name) + * #{fontcolour=FontColourName}: retrieves the value of the fallback setting "FontColor_color_" from openmw.cfg, + * in the format "r g b a", float values in range 0-1. Useful for "Colour" and "TextColour" properties in skins. + * #{fontcolourhtml=FontColourName}: retrieves the value of the fallback setting "FontColor_color_" from openmw.cfg, + * in the format "#xxxxxx" where x are hexadecimal numbers. Useful in an EditBox's caption to change the color of following text. */ void onRetrieveTag(const MyGUI::UString& _tag, MyGUI::UString& _result); void onCursorChange(const std::string& name); void onKeyFocusChanged(MyGUI::Widget* widget); + // Key pressed while playing a video + void onVideoKeyPressed(MyGUI::Widget *_sender, MyGUI::KeyCode _key, MyGUI::Char _char); + void sizeVideo(int screenWidth, int screenHeight); + + void onClipboardChanged(const std::string& _type, const std::string& _data); + void onClipboardRequested(const std::string& _type, std::string& _data); }; } diff --git a/apps/openmw/mwgui/windowpinnablebase.cpp b/apps/openmw/mwgui/windowpinnablebase.cpp index 47364337c..f9bbca665 100644 --- a/apps/openmw/mwgui/windowpinnablebase.cpp +++ b/apps/openmw/mwgui/windowpinnablebase.cpp @@ -7,10 +7,21 @@ namespace MWGui WindowPinnableBase::WindowPinnableBase(const std::string& parLayout) : WindowBase(parLayout), mPinned(false) { - ExposedWindow* window = static_cast(mMainWidget); + ExposedWindow* window = mMainWidget->castType(); mPinButton = window->getSkinWidget ("Button"); mPinButton->eventMouseButtonClick += MyGUI::newDelegate(this, &WindowPinnableBase::onPinButtonClicked); + + MyGUI::Button* button = NULL; + MyGUI::VectorWidgetPtr widgets = window->getSkinWidgetsByName("Action"); + for (MyGUI::VectorWidgetPtr::iterator it = widgets.begin(); it != widgets.end(); ++it) + { + if ((*it)->isUserString("HideWindowOnDoubleClick")) + button = (*it)->castType(); + } + + if (button) + button->eventMouseButtonDoubleClick += MyGUI::newDelegate(this, &WindowPinnableBase::onDoubleClick); } void WindowPinnableBase::onPinButtonClicked(MyGUI::Widget* _sender) @@ -25,6 +36,17 @@ namespace MWGui onPinToggled(); } + void WindowPinnableBase::onDoubleClick(MyGUI::Widget *_sender) + { + onTitleDoubleClicked(); + } + + void WindowPinnableBase::setPinned(bool pinned) + { + if (pinned != mPinned) + onPinButtonClicked(mPinButton); + } + void WindowPinnableBase::setPinButtonVisible(bool visible) { mPinButton->setVisible(visible); diff --git a/apps/openmw/mwgui/windowpinnablebase.hpp b/apps/openmw/mwgui/windowpinnablebase.hpp index cd393f918..8b7bbefaf 100644 --- a/apps/openmw/mwgui/windowpinnablebase.hpp +++ b/apps/openmw/mwgui/windowpinnablebase.hpp @@ -12,13 +12,16 @@ namespace MWGui public: WindowPinnableBase(const std::string& parLayout); bool pinned() { return mPinned; } + void setPinned (bool pinned); void setPinButtonVisible(bool visible); private: void onPinButtonClicked(MyGUI::Widget* _sender); + void onDoubleClick(MyGUI::Widget* _sender); protected: virtual void onPinToggled() = 0; + virtual void onTitleDoubleClicked() = 0; MyGUI::Widget* mPinButton; bool mPinned; diff --git a/apps/openmw/mwinput/inputmanagerimp.cpp b/apps/openmw/mwinput/inputmanagerimp.cpp index 8cfe2c2b3..29b166a6a 100644 --- a/apps/openmw/mwinput/inputmanagerimp.cpp +++ b/apps/openmw/mwinput/inputmanagerimp.cpp @@ -26,7 +26,11 @@ #include "../mwworld/inventorystore.hpp" #include "../mwworld/esmstore.hpp" -#include "../mwmechanics/creaturestats.hpp" +#include "../mwmechanics/npcstats.hpp" + +#include "../mwdialogue/dialoguemanagerimp.hpp" + +#include "../mwgui/windowbase.hpp" using namespace ICS; @@ -113,7 +117,8 @@ namespace MWInput , mPreviewPOVDelay(0.f) , mTimeIdle(0.f) , mOverencumberedMessageDelay(0.f) - , mAlwaysRunActive(false) + , mAlwaysRunActive(Settings::Manager::getBool("always run", "Input")) + , mAttemptJump(false) , mControlsDisabled(false) { @@ -161,6 +166,21 @@ namespace MWInput delete mInputManager; } + void InputManager::setPlayerControlsEnabled(bool enabled) + { + int nPlayerChannels = 17; + int playerChannels[] = {A_Activate, A_AutoMove, A_AlwaysRun, A_ToggleWeapon, + A_ToggleSpell, A_Rest, A_QuickKey1, A_QuickKey2, + A_QuickKey3, A_QuickKey4, A_QuickKey5, A_QuickKey6, + A_QuickKey7, A_QuickKey8, A_QuickKey9, A_QuickKey10, + A_Use}; + + for(int i = 0; i < nPlayerChannels; i++) { + int pc = playerChannels[i]; + mInputBinder->getChannel(pc)->setEnabled(enabled); + } + } + void InputManager::channelChanged(ICS::Channel* channel, float currentValue, float previousValue) { if (mDragDrop) @@ -175,6 +195,11 @@ namespace MWInput mPlayer->getPlayer().getClass().getCreatureStats(mPlayer->getPlayer()).setAttackingOrSpell(currentValue); } + if (action == A_Jump) + { + mAttemptJump = (currentValue == 1.0 && previousValue == 0.0); + } + if (currentValue == 1) { // trigger action activated @@ -252,7 +277,10 @@ namespace MWInput showQuickKeysMenu(); break; case A_ToggleHUD: - MWBase::Environment::get().getWindowManager()->toggleHud(); + MWBase::Environment::get().getWindowManager()->toggleGui(); + break; + case A_ToggleDebug: + MWBase::Environment::get().getWindowManager()->toggleDebugWindow(); break; case A_QuickSave: quickSave(); @@ -301,107 +329,107 @@ namespace MWInput } // Disable movement in Gui mode - if (MWBase::Environment::get().getWindowManager()->isGuiMode() - || MWBase::Environment::get().getStateManager()->getState() != MWBase::StateManager::State_Running) - return; - - - // Configure player movement according to keyboard input. Actual movement will - // be done in the physics system. - if (mControlSwitch["playercontrols"]) + if (!(MWBase::Environment::get().getWindowManager()->isGuiMode() + || MWBase::Environment::get().getStateManager()->getState() != MWBase::StateManager::State_Running)) { - bool triedToMove = false; - if (actionIsActive(A_MoveLeft)) + // Configure player movement according to keyboard input. Actual movement will + // be done in the physics system. + if (mControlSwitch["playercontrols"]) { - triedToMove = true; - mPlayer->setLeftRight (-1); - } - else if (actionIsActive(A_MoveRight)) - { - triedToMove = true; - mPlayer->setLeftRight (1); - } + bool triedToMove = false; + if (actionIsActive(A_MoveLeft)) + { + triedToMove = true; + mPlayer->setLeftRight (-1); + } + else if (actionIsActive(A_MoveRight)) + { + triedToMove = true; + mPlayer->setLeftRight (1); + } - if (actionIsActive(A_MoveForward)) - { - triedToMove = true; - mPlayer->setAutoMove (false); - mPlayer->setForwardBackward (1); - } - else if (actionIsActive(A_MoveBackward)) - { - triedToMove = true; - mPlayer->setAutoMove (false); - mPlayer->setForwardBackward (-1); - } + if (actionIsActive(A_MoveForward)) + { + triedToMove = true; + mPlayer->setAutoMove (false); + mPlayer->setForwardBackward (1); + } + else if (actionIsActive(A_MoveBackward)) + { + triedToMove = true; + mPlayer->setAutoMove (false); + mPlayer->setForwardBackward (-1); + } - else if(mPlayer->getAutoMove()) - { - triedToMove = true; - mPlayer->setForwardBackward (1); - } + else if(mPlayer->getAutoMove()) + { + triedToMove = true; + mPlayer->setForwardBackward (1); + } - mPlayer->setSneak(actionIsActive(A_Sneak)); + mPlayer->setSneak(actionIsActive(A_Sneak)); - if (actionIsActive(A_Jump) && mControlSwitch["playerjumping"]) - { - mPlayer->setUpDown (1); - triedToMove = true; - } + if (mAttemptJump && mControlSwitch["playerjumping"]) + { + mPlayer->setUpDown (1); + triedToMove = true; + } - if (mAlwaysRunActive) - mPlayer->setRunState(!actionIsActive(A_Run)); - else - mPlayer->setRunState(actionIsActive(A_Run)); + if (mAlwaysRunActive) + mPlayer->setRunState(!actionIsActive(A_Run)); + else + mPlayer->setRunState(actionIsActive(A_Run)); - // if player tried to start moving, but can't (due to being overencumbered), display a notification. - if (triedToMove) - { - MWWorld::Ptr player = MWBase::Environment::get().getWorld ()->getPlayerPtr(); - mOverencumberedMessageDelay -= dt; - if (player.getClass().getEncumbrance(player) >= player.getClass().getCapacity(player)) + // if player tried to start moving, but can't (due to being overencumbered), display a notification. + if (triedToMove) { - mPlayer->setAutoMove (false); - if (mOverencumberedMessageDelay <= 0) + MWWorld::Ptr player = MWBase::Environment::get().getWorld ()->getPlayerPtr(); + mOverencumberedMessageDelay -= dt; + if (player.getClass().getEncumbrance(player) > player.getClass().getCapacity(player)) { - MWBase::Environment::get().getWindowManager ()->messageBox("#{sNotifyMessage59}"); - mOverencumberedMessageDelay = 1.0; + mPlayer->setAutoMove (false); + if (mOverencumberedMessageDelay <= 0) + { + MWBase::Environment::get().getWindowManager ()->messageBox("#{sNotifyMessage59}"); + mOverencumberedMessageDelay = 1.0; + } } } - } - - if (mControlSwitch["playerviewswitch"]) { - // work around preview mode toggle when pressing Alt+Tab - if (actionIsActive(A_TogglePOV) && !mInputManager->isModifierHeld(SDL_Keymod(KMOD_ALT))) { - if (mPreviewPOVDelay <= 0.5 && - (mPreviewPOVDelay += dt) > 0.5) - { - mPreviewPOVDelay = 1.f; - MWBase::Environment::get().getWorld()->togglePreviewMode(true); - } - } else { - //disable preview mode - MWBase::Environment::get().getWorld()->togglePreviewMode(false); - if (mPreviewPOVDelay > 0.f && mPreviewPOVDelay <= 0.5) { - MWBase::Environment::get().getWorld()->togglePOV(); + if (mControlSwitch["playerviewswitch"]) { + + // work around preview mode toggle when pressing Alt+Tab + if (actionIsActive(A_TogglePOV) && !mInputManager->isModifierHeld(SDL_Keymod(KMOD_ALT))) { + if (mPreviewPOVDelay <= 0.5 && + (mPreviewPOVDelay += dt) > 0.5) + { + mPreviewPOVDelay = 1.f; + MWBase::Environment::get().getWorld()->togglePreviewMode(true); + } + } else { + //disable preview mode + MWBase::Environment::get().getWorld()->togglePreviewMode(false); + if (mPreviewPOVDelay > 0.f && mPreviewPOVDelay <= 0.5) { + MWBase::Environment::get().getWorld()->togglePOV(); + } + mPreviewPOVDelay = 0.f; } - mPreviewPOVDelay = 0.f; } } + if (actionIsActive(A_MoveForward) || + actionIsActive(A_MoveBackward) || + actionIsActive(A_MoveLeft) || + actionIsActive(A_MoveRight) || + actionIsActive(A_Jump) || + actionIsActive(A_Sneak) || + actionIsActive(A_TogglePOV)) + { + resetIdleTime(); + } else { + updateIdleTime(dt); + } } - if (actionIsActive(A_MoveForward) || - actionIsActive(A_MoveBackward) || - actionIsActive(A_MoveLeft) || - actionIsActive(A_MoveRight) || - actionIsActive(A_Jump) || - actionIsActive(A_Sneak) || - actionIsActive(A_TogglePOV)) - { - resetIdleTime(); - } else { - updateIdleTime(dt); - } + mAttemptJump = false; // Can only jump on first frame input is on } void InputManager::setDragDrop(bool dragDrop) @@ -473,53 +501,25 @@ namespace MWInput void InputManager::keyPressed( const SDL_KeyboardEvent &arg ) { - // Cut, copy & paste - MyGUI::Widget* focus = MyGUI::InputManager::getInstance().getKeyFocusWidget(); - if (focus) - { - MyGUI::EditBox* edit = focus->castType(false); - if (edit && !edit->getEditReadOnly()) - { - if (arg.keysym.sym == SDLK_v && (arg.keysym.mod & SDL_Keymod(KMOD_CTRL))) - { - char* text = SDL_GetClipboardText(); - - if (text) - { - edit->insertText(MyGUI::UString(text), edit->getTextCursor()); - SDL_free(text); - } - } - if (arg.keysym.sym == SDLK_x && (arg.keysym.mod & SDL_Keymod(KMOD_CTRL))) - { - // Discard color codes and other escape characters - std::string text = MyGUI::TextIterator::getOnlyText(edit->getTextSelection()); - if (text.length()) - { - SDL_SetClipboardText(text.c_str()); - edit->deleteTextSelection(); - } - } - } - if (edit && !edit->getEditStatic()) - { - if (arg.keysym.sym == SDLK_c && (arg.keysym.mod & SDL_Keymod(KMOD_CTRL))) - { - // Discard color codes and other escape characters - std::string text = MyGUI::TextIterator::getOnlyText(edit->getTextSelection()); - if (text.length()) - SDL_SetClipboardText(text.c_str()); - } - } - } - - if (!mControlsDisabled) - mInputBinder->keyPressed (arg); - + // HACK: to make Morrowind's default keybinding for the console work without printing an extra "^" upon closing + // This assumes that SDL_TextInput events always come *after* the key event + // (which is somewhat reasonable, and hopefully true for all SDL platforms) OIS::KeyCode kc = mInputManager->sdl2OISKeyCode(arg.keysym.sym); + if (mInputBinder->getKeyBinding(mInputBinder->getControl(A_Console), ICS::Control::INCREASE) + == arg.keysym.scancode + && MWBase::Environment::get().getWindowManager()->getMode() == MWGui::GM_Console) + SDL_StopTextInput(); + bool consumed = false; if (kc != OIS::KC_UNASSIGNED) - MyGUI::InputManager::getInstance().injectKeyPress(MyGUI::KeyCode::Enum(kc), 0); + { + consumed = SDL_IsTextInputActive() && + ( !(SDLK_SCANCODE_MASK & arg.keysym.sym) && std::isprint(arg.keysym.sym)); // Little trick to check if key is printable + bool guiFocus = MyGUI::InputManager::getInstance().injectKeyPress(MyGUI::KeyCode::Enum(kc), 0); + setPlayerControlsEnabled(!guiFocus); + } + if (!mControlsDisabled && !consumed) + mInputBinder->keyPressed (arg); } void InputManager::textInput(const SDL_TextInputEvent &arg) @@ -532,36 +532,52 @@ namespace MWInput void InputManager::keyReleased(const SDL_KeyboardEvent &arg ) { - mInputBinder->keyReleased (arg); - OIS::KeyCode kc = mInputManager->sdl2OISKeyCode(arg.keysym.sym); - MyGUI::InputManager::getInstance().injectKeyRelease(MyGUI::KeyCode::Enum(kc)); + setPlayerControlsEnabled(!MyGUI::InputManager::getInstance().injectKeyRelease(MyGUI::KeyCode::Enum(kc))); + mInputBinder->keyReleased (arg); } void InputManager::mousePressed( const SDL_MouseButtonEvent &arg, Uint8 id ) { - mInputBinder->mousePressed (arg, id); - - if (id != SDL_BUTTON_LEFT && id != SDL_BUTTON_RIGHT) - return; // MyGUI has no use for these events + bool guiMode = false; - MyGUI::InputManager::getInstance().injectMousePress(mMouseX, mMouseY, sdlButtonToMyGUI(id)); - if (MyGUI::InputManager::getInstance ().getMouseFocusWidget () != 0) + if (id == SDL_BUTTON_LEFT || id == SDL_BUTTON_RIGHT) // MyGUI only uses these mouse events { - MyGUI::Button* b = MyGUI::InputManager::getInstance ().getMouseFocusWidget ()->castType(false); - if (b && b->getEnabled()) + guiMode = MWBase::Environment::get().getWindowManager()->isGuiMode(); + guiMode = MyGUI::InputManager::getInstance().injectMousePress(mMouseX, mMouseY, sdlButtonToMyGUI(id)) && guiMode; + if (MyGUI::InputManager::getInstance ().getMouseFocusWidget () != 0) { - MWBase::Environment::get().getSoundManager ()->playSound ("Menu Click", 1.f, 1.f); + MyGUI::Button* b = MyGUI::InputManager::getInstance ().getMouseFocusWidget ()->castType(false); + if (b && b->getEnabled()) + { + MWBase::Environment::get().getSoundManager ()->playSound ("Menu Click", 1.f, 1.f); + } } } + + setPlayerControlsEnabled(!guiMode); + + // Don't trigger any mouse bindings while in settings menu, otherwise rebinding controls becomes impossible + if (MWBase::Environment::get().getWindowManager()->getMode() != MWGui::GM_Settings) + mInputBinder->mousePressed (arg, id); } void InputManager::mouseReleased( const SDL_MouseButtonEvent &arg, Uint8 id ) - { - mInputBinder->mouseReleased (arg, id); + { - MyGUI::InputManager::getInstance().injectMouseRelease(mMouseX, mMouseY, sdlButtonToMyGUI(id)); + if(mInputBinder->detectingBindingState()) + { + mInputBinder->mouseReleased (arg, id); + } else { + bool guiMode = MWBase::Environment::get().getWindowManager()->isGuiMode(); + guiMode = MyGUI::InputManager::getInstance().injectMouseRelease(mMouseX, mMouseY, sdlButtonToMyGUI(id)) && guiMode; + + if(mInputBinder->detectingBindingState()) return; // don't allow same mouseup to bind as initiated bind + + setPlayerControlsEnabled(!guiMode); + mInputBinder->mouseReleased (arg, id); + } } void InputManager::mouseMoved(const SFO::MouseMotionEvent &arg ) @@ -635,27 +651,29 @@ namespace MWInput void InputManager::toggleMainMenu() { - if (MyGUI::InputManager::getInstance ().isModalAny()) + if (MyGUI::InputManager::getInstance().isModalAny()) { + MWBase::Environment::get().getWindowManager()->getCurrentModal()->exit(); return; + } - if (MWBase::Environment::get().getWindowManager()->containsMode(MWGui::GM_MainMenu)) + if(!MWBase::Environment::get().getWindowManager()->isGuiMode()) //No open GUIs, open up the MainMenu { - MWBase::Environment::get().getWindowManager()->popGuiMode(); - MWBase::Environment::get().getSoundManager()->resumeSounds (MWBase::SoundManager::Play_TypeSfx); + MWBase::Environment::get().getWindowManager()->pushGuiMode (MWGui::GM_MainMenu); } - else + else //Close current GUI { - MWBase::Environment::get().getWindowManager()->pushGuiMode (MWGui::GM_MainMenu); - MWBase::Environment::get().getSoundManager()->pauseSounds (MWBase::SoundManager::Play_TypeSfx); + MWBase::Environment::get().getWindowManager()->exitCurrentGuiMode(); } } void InputManager::quickLoad() { - MWBase::Environment::get().getStateManager()->quickLoad(); + if (!MyGUI::InputManager::getInstance().isModalAny()) + MWBase::Environment::get().getStateManager()->quickLoad(); } void InputManager::quickSave() { - MWBase::Environment::get().getStateManager()->quickSave(); + if (!MyGUI::InputManager::getInstance().isModalAny()) + MWBase::Environment::get().getStateManager()->quickSave(); } void InputManager::toggleSpell() { @@ -755,22 +773,28 @@ namespace MWInput if (MyGUI::InputManager::getInstance ().isModalAny()) return; - // Toggle between game mode and journal mode - if(!MWBase::Environment::get().getWindowManager()->isGuiMode() && MWBase::Environment::get().getWindowManager ()->getJournalAllowed()) + if(MWBase::Environment::get().getWindowManager()->getMode() != MWGui::GM_Journal + && MWBase::Environment::get().getWindowManager ()->getJournalAllowed()) { MWBase::Environment::get().getSoundManager()->playSound ("book open", 1.0, 1.0); MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_Journal); } - else if(MWBase::Environment::get().getWindowManager()->getMode() == MWGui::GM_Journal) + else if(MWBase::Environment::get().getWindowManager()->containsMode(MWGui::GM_Journal)) { - MWBase::Environment::get().getSoundManager()->playSound ("book close", 1.0, 1.0); - MWBase::Environment::get().getWindowManager()->popGuiMode(); + MWBase::Environment::get().getWindowManager()->removeGuiMode(MWGui::GM_Journal); } - // .. but don't touch any other mode. } void InputManager::quickKey (int index) { + MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); + if (player.getClass().getNpcStats(player).isWerewolf()) + { + // Cannot use items or spells while in werewolf form + MWBase::Environment::get().getWindowManager()->messageBox("#{sWerewolfRefusal}"); + return; + } + if (!MWBase::Environment::get().getWindowManager()->isGuiMode()) MWBase::Environment::get().getWindowManager()->activateQuickKey (index); } @@ -779,9 +803,24 @@ namespace MWInput { if (!MWBase::Environment::get().getWindowManager()->isGuiMode () && MWBase::Environment::get().getWorld()->getGlobalFloat ("chargenstate")==-1) + { + MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); + if (player.getClass().getNpcStats(player).isWerewolf()) + { + // Cannot use items or spells while in werewolf form + MWBase::Environment::get().getWindowManager()->messageBox("#{sWerewolfRefusal}"); + return; + } + MWBase::Environment::get().getWindowManager()->pushGuiMode (MWGui::GM_QuickKeysMenu); - else if (MWBase::Environment::get().getWindowManager()->getMode () == MWGui::GM_QuickKeysMenu) - MWBase::Environment::get().getWindowManager()->removeGuiMode (MWGui::GM_QuickKeysMenu); + + } + else if (MWBase::Environment::get().getWindowManager()->getMode () == MWGui::GM_QuickKeysMenu) { + while(MyGUI::InputManager::getInstance().isModalAny()) { //Handle any open Modal windows + MWBase::Environment::get().getWindowManager()->getCurrentModal()->exit(); + } + MWBase::Environment::get().getWindowManager()->exitCurrentGuiMode(); //And handle the actual main window + } } void InputManager::activate() @@ -802,6 +841,8 @@ namespace MWInput { if (MWBase::Environment::get().getWindowManager()->isGuiMode()) return; mAlwaysRunActive = !mAlwaysRunActive; + + Settings::Manager::setBool("always run", "Input", mAlwaysRunActive); } void InputManager::resetIdleTime() @@ -832,41 +873,42 @@ namespace MWInput { // using hardcoded key defaults is inevitable, if we want the configuration files to stay valid // across different versions of OpenMW (in the case where another input action is added) - std::map defaultKeyBindings; + std::map defaultKeyBindings; //Gets the Keyvalue from the Scancode; gives the button in the same place reguardless of keyboard format - defaultKeyBindings[A_Activate] = SDL_GetKeyFromScancode(SDL_SCANCODE_SPACE); - defaultKeyBindings[A_MoveBackward] = SDL_GetKeyFromScancode(SDL_SCANCODE_S); - defaultKeyBindings[A_MoveForward] = SDL_GetKeyFromScancode(SDL_SCANCODE_W); - defaultKeyBindings[A_MoveLeft] = SDL_GetKeyFromScancode(SDL_SCANCODE_A); - defaultKeyBindings[A_MoveRight] = SDL_GetKeyFromScancode(SDL_SCANCODE_D); - defaultKeyBindings[A_ToggleWeapon] = SDL_GetKeyFromScancode(SDL_SCANCODE_F); - defaultKeyBindings[A_ToggleSpell] = SDL_GetKeyFromScancode(SDL_SCANCODE_R); - defaultKeyBindings[A_QuickKeysMenu] = SDL_GetKeyFromScancode(SDL_SCANCODE_F1); - defaultKeyBindings[A_Console] = SDL_GetKeyFromScancode(SDL_SCANCODE_F2); - defaultKeyBindings[A_Run] = SDL_GetKeyFromScancode(SDL_SCANCODE_LSHIFT); - defaultKeyBindings[A_Sneak] = SDL_GetKeyFromScancode(SDL_SCANCODE_LCTRL); - defaultKeyBindings[A_AutoMove] = SDL_GetKeyFromScancode(SDL_SCANCODE_Q); - defaultKeyBindings[A_Jump] = SDL_GetKeyFromScancode(SDL_SCANCODE_E); - defaultKeyBindings[A_Journal] = SDL_GetKeyFromScancode(SDL_SCANCODE_J); - defaultKeyBindings[A_Rest] = SDL_GetKeyFromScancode(SDL_SCANCODE_T); - defaultKeyBindings[A_GameMenu] = SDL_GetKeyFromScancode(SDL_SCANCODE_ESCAPE); - defaultKeyBindings[A_TogglePOV] = SDL_GetKeyFromScancode(SDL_SCANCODE_TAB); - defaultKeyBindings[A_QuickKey1] = SDL_GetKeyFromScancode(SDL_SCANCODE_1); - defaultKeyBindings[A_QuickKey2] = SDL_GetKeyFromScancode(SDL_SCANCODE_2); - defaultKeyBindings[A_QuickKey3] = SDL_GetKeyFromScancode(SDL_SCANCODE_3); - defaultKeyBindings[A_QuickKey4] = SDL_GetKeyFromScancode(SDL_SCANCODE_4); - defaultKeyBindings[A_QuickKey5] = SDL_GetKeyFromScancode(SDL_SCANCODE_5); - defaultKeyBindings[A_QuickKey6] = SDL_GetKeyFromScancode(SDL_SCANCODE_6); - defaultKeyBindings[A_QuickKey7] = SDL_GetKeyFromScancode(SDL_SCANCODE_7); - defaultKeyBindings[A_QuickKey8] = SDL_GetKeyFromScancode(SDL_SCANCODE_8); - defaultKeyBindings[A_QuickKey9] = SDL_GetKeyFromScancode(SDL_SCANCODE_9); - defaultKeyBindings[A_QuickKey10] = SDL_GetKeyFromScancode(SDL_SCANCODE_0); - defaultKeyBindings[A_Screenshot] = SDL_GetKeyFromScancode(SDL_SCANCODE_F12); - defaultKeyBindings[A_ToggleHUD] = SDL_GetKeyFromScancode(SDL_SCANCODE_F11); - defaultKeyBindings[A_AlwaysRun] = SDL_GetKeyFromScancode(SDL_SCANCODE_Y); - defaultKeyBindings[A_QuickSave] = SDL_GetKeyFromScancode(SDL_SCANCODE_F5); - defaultKeyBindings[A_QuickLoad] = SDL_GetKeyFromScancode(SDL_SCANCODE_F9); + defaultKeyBindings[A_Activate] = SDL_SCANCODE_SPACE; + defaultKeyBindings[A_MoveBackward] = SDL_SCANCODE_S; + defaultKeyBindings[A_MoveForward] = SDL_SCANCODE_W; + defaultKeyBindings[A_MoveLeft] = SDL_SCANCODE_A; + defaultKeyBindings[A_MoveRight] = SDL_SCANCODE_D; + defaultKeyBindings[A_ToggleWeapon] = SDL_SCANCODE_F; + defaultKeyBindings[A_ToggleSpell] = SDL_SCANCODE_R; + defaultKeyBindings[A_QuickKeysMenu] = SDL_SCANCODE_F1; + defaultKeyBindings[A_Console] = SDL_SCANCODE_GRAVE; + defaultKeyBindings[A_Run] = SDL_SCANCODE_LSHIFT; + defaultKeyBindings[A_Sneak] = SDL_SCANCODE_LCTRL; + defaultKeyBindings[A_AutoMove] = SDL_SCANCODE_Q; + defaultKeyBindings[A_Jump] = SDL_SCANCODE_E; + defaultKeyBindings[A_Journal] = SDL_SCANCODE_J; + defaultKeyBindings[A_Rest] = SDL_SCANCODE_T; + defaultKeyBindings[A_GameMenu] = SDL_SCANCODE_ESCAPE; + defaultKeyBindings[A_TogglePOV] = SDL_SCANCODE_TAB; + defaultKeyBindings[A_QuickKey1] = SDL_SCANCODE_1; + defaultKeyBindings[A_QuickKey2] = SDL_SCANCODE_2; + defaultKeyBindings[A_QuickKey3] = SDL_SCANCODE_3; + defaultKeyBindings[A_QuickKey4] = SDL_SCANCODE_4; + defaultKeyBindings[A_QuickKey5] = SDL_SCANCODE_5; + defaultKeyBindings[A_QuickKey6] = SDL_SCANCODE_6; + defaultKeyBindings[A_QuickKey7] = SDL_SCANCODE_7; + defaultKeyBindings[A_QuickKey8] = SDL_SCANCODE_8; + defaultKeyBindings[A_QuickKey9] = SDL_SCANCODE_9; + defaultKeyBindings[A_QuickKey10] = SDL_SCANCODE_0; + defaultKeyBindings[A_Screenshot] = SDL_SCANCODE_F12; + defaultKeyBindings[A_ToggleHUD] = SDL_SCANCODE_F11; + defaultKeyBindings[A_ToggleDebug] = SDL_SCANCODE_F10; + defaultKeyBindings[A_AlwaysRun] = SDL_SCANCODE_CAPSLOCK; + defaultKeyBindings[A_QuickSave] = SDL_SCANCODE_F5; + defaultKeyBindings[A_QuickLoad] = SDL_SCANCODE_F9; std::map defaultMouseButtonBindings; defaultMouseButtonBindings[A_Inventory] = SDL_BUTTON_RIGHT; @@ -888,23 +930,20 @@ namespace MWInput } if (!controlExists || force || - ( mInputBinder->getKeyBinding (control, ICS::Control::INCREASE) == SDLK_UNKNOWN + ( mInputBinder->getKeyBinding (control, ICS::Control::INCREASE) == SDL_SCANCODE_UNKNOWN && mInputBinder->getMouseButtonBinding (control, ICS::Control::INCREASE) == ICS_MAX_DEVICE_BUTTONS )) { clearAllBindings (control); - if (defaultKeyBindings.find(i) != defaultKeyBindings.end()) - mInputBinder->addKeyBinding(control, static_cast(defaultKeyBindings[i]), ICS::Control::INCREASE); - else if (defaultMouseButtonBindings.find(i) != defaultMouseButtonBindings.end()) + if (defaultKeyBindings.find(i) != defaultKeyBindings.end() + && !mInputBinder->isKeyBound(defaultKeyBindings[i])) + mInputBinder->addKeyBinding(control, defaultKeyBindings[i], ICS::Control::INCREASE); + else if (defaultMouseButtonBindings.find(i) != defaultMouseButtonBindings.end() + && !mInputBinder->isMouseButtonBound(defaultMouseButtonBindings[i])) mInputBinder->addMouseButtonBinding (control, defaultMouseButtonBindings[i], ICS::Control::INCREASE); } } - - // Printscreen key should not be allowed because it's captured by system screenshot function - // We check this explicitely here to fix up pre-0.26 config files. Can be removed after a few versions - if (mInputBinder->getKeyBinding(mInputBinder->getControl(A_Screenshot), ICS::Control::INCREASE) == SDLK_PRINTSCREEN) - mInputBinder->addKeyBinding(mInputBinder->getControl(A_Screenshot), SDLK_F12, ICS::Control::INCREASE); } std::string InputManager::getActionDescription (int action) @@ -959,8 +998,8 @@ namespace MWInput ICS::Control* c = mInputBinder->getChannel (action)->getAttachedControls ().front().control; - if (mInputBinder->getKeyBinding (c, ICS::Control::INCREASE) != SDLK_UNKNOWN) - return mInputBinder->keyCodeToString (mInputBinder->getKeyBinding (c, ICS::Control::INCREASE)); + if (mInputBinder->getKeyBinding (c, ICS::Control::INCREASE) != SDL_SCANCODE_UNKNOWN) + return mInputBinder->scancodeToString (mInputBinder->getKeyBinding (c, ICS::Control::INCREASE)); else if (mInputBinder->getMouseButtonBinding (c, ICS::Control::INCREASE) != ICS_MAX_DEVICE_BUTTONS) return "#{sMouse} " + boost::lexical_cast(mInputBinder->getMouseButtonBinding (c, ICS::Control::INCREASE)); else @@ -1021,10 +1060,10 @@ namespace MWInput } void InputManager::keyBindingDetected(ICS::InputControlSystem* ICS, ICS::Control* control - , SDL_Keycode key, ICS::Control::ControlChangingDirection direction) + , SDL_Scancode key, ICS::Control::ControlChangingDirection direction) { //Disallow binding escape key - if(key==SDLK_ESCAPE) + if(key==SDL_SCANCODE_ESCAPE) return; clearAllBindings(control); @@ -1075,7 +1114,7 @@ namespace MWInput void InputManager::clearAllBindings (ICS::Control* control) { // right now we don't really need multiple bindings for the same action, so remove all others first - if (mInputBinder->getKeyBinding (control, ICS::Control::INCREASE) != SDLK_UNKNOWN) + if (mInputBinder->getKeyBinding (control, ICS::Control::INCREASE) != SDL_SCANCODE_UNKNOWN) mInputBinder->removeKeyBinding (mInputBinder->getKeyBinding (control, ICS::Control::INCREASE)); if (mInputBinder->getMouseButtonBinding (control, ICS::Control::INCREASE) != ICS_MAX_DEVICE_BUTTONS) mInputBinder->removeMouseButtonBinding (mInputBinder->getMouseButtonBinding (control, ICS::Control::INCREASE)); diff --git a/apps/openmw/mwinput/inputmanagerimp.hpp b/apps/openmw/mwinput/inputmanagerimp.hpp index 3787a9c07..346e02ff9 100644 --- a/apps/openmw/mwinput/inputmanagerimp.hpp +++ b/apps/openmw/mwinput/inputmanagerimp.hpp @@ -108,7 +108,7 @@ namespace MWInput , ICS::InputControlSystem::NamedAxis axis, ICS::Control::ControlChangingDirection direction); virtual void keyBindingDetected(ICS::InputControlSystem* ICS, ICS::Control* control - , SDL_Keycode key, ICS::Control::ControlChangingDirection direction); + , SDL_Scancode key, ICS::Control::ControlChangingDirection direction); virtual void mouseButtonBindingDetected(ICS::InputControlSystem* ICS, ICS::Control* control , unsigned int button, ICS::Control::ControlChangingDirection direction); @@ -163,6 +163,7 @@ namespace MWInput int mMouseWheel; bool mUserFileExists; bool mAlwaysRunActive; + bool mAttemptJump; std::map mControlSwitch; @@ -173,6 +174,8 @@ namespace MWInput void resetIdleTime(); void updateIdleTime(float dt); + void setPlayerControlsEnabled(bool enabled); + private: void toggleMainMenu(); void toggleSpell(); @@ -256,6 +259,8 @@ namespace MWInput A_ToggleHUD, + A_ToggleDebug, + A_Last // Marker for the last item }; }; diff --git a/apps/openmw/mwmechanics/activespells.hpp b/apps/openmw/mwmechanics/activespells.hpp index 6c4c93128..9c1a5e613 100644 --- a/apps/openmw/mwmechanics/activespells.hpp +++ b/apps/openmw/mwmechanics/activespells.hpp @@ -40,6 +40,10 @@ namespace MWMechanics void readState (const ESM::ActiveSpells& state); void writeState (ESM::ActiveSpells& state) const; + TIterator begin() const; + + TIterator end() const; + private: mutable TContainer mSpells; @@ -57,10 +61,6 @@ namespace MWMechanics const TContainer& getActiveSpells() const; - TIterator begin() const; - - TIterator end() const; - public: ActiveSpells(); diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp index 8dcccfdb3..2e835d57e 100644 --- a/apps/openmw/mwmechanics/actors.cpp +++ b/apps/openmw/mwmechanics/actors.cpp @@ -20,9 +20,12 @@ #include "../mwbase/windowmanager.hpp" #include "../mwbase/soundmanager.hpp" +#include "../mwrender/animation.hpp" + #include "npcstats.hpp" #include "creaturestats.hpp" #include "movement.hpp" +#include "character.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" @@ -87,12 +90,64 @@ bool disintegrateSlot (MWWorld::Ptr ptr, int slot, float disintegrate) return false; } +class CheckActorCommanded : public MWMechanics::EffectSourceVisitor +{ + MWWorld::Ptr mActor; +public: + bool mCommanded; + CheckActorCommanded(MWWorld::Ptr actor) + : mActor(actor) + , mCommanded(false){} + + virtual void visit (MWMechanics::EffectKey key, + const std::string& sourceName, int casterActorId, + float magnitude, float remainingTime = -1) + { + MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); + if ( ((key.mId == ESM::MagicEffect::CommandHumanoid && mActor.getClass().isNpc()) + || (key.mId == ESM::MagicEffect::CommandCreature && mActor.getTypeName() == typeid(ESM::Creature).name())) + && casterActorId == player.getClass().getCreatureStats(player).getActorId() + && magnitude >= mActor.getClass().getCreatureStats(mActor).getLevel()) + mCommanded = true; + } +}; + +void adjustCommandedActor (const MWWorld::Ptr& actor) +{ + CheckActorCommanded check(actor); + MWMechanics::CreatureStats& stats = actor.getClass().getCreatureStats(actor); + stats.getActiveSpells().visitEffectSources(check); + + bool hasCommandPackage = false; + + std::list::const_iterator it; + for (it = stats.getAiSequence().begin(); it != stats.getAiSequence().end(); ++it) + { + if ((*it)->getTypeId() == MWMechanics::AiPackage::TypeIdFollow && + dynamic_cast(*it)->isCommanded()) + { + hasCommandPackage = true; + break; + } + } + + if (check.mCommanded && !hasCommandPackage) + { + MWMechanics::AiFollow package("player", true); + stats.getAiSequence().stack(package, actor); + } + else if (!check.mCommanded && hasCommandPackage) + { + stats.getAiSequence().erase(it); + } +} + void getRestorationPerHourOfSleep (const MWWorld::Ptr& ptr, float& health, float& magicka) { MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats (ptr); const MWWorld::Store& settings = MWBase::Environment::get().getWorld()->getStore().get(); - bool stunted = stats.getMagicEffects ().get(ESM::MagicEffect::StuntedMagicka).mMagnitude > 0; + bool stunted = stats.getMagicEffects ().get(ESM::MagicEffect::StuntedMagicka).getMagnitude() > 0; int endurance = stats.getAttribute (ESM::Attribute::Endurance).getModified (); health = 0.1 * endurance; @@ -105,6 +160,30 @@ void getRestorationPerHourOfSleep (const MWWorld::Ptr& ptr, float& health, float } } +void cleanupSummonedCreature (MWMechanics::CreatureStats& casterStats, int creatureActorId) +{ + MWWorld::Ptr ptr = MWBase::Environment::get().getWorld()->searchPtrViaActorId(creatureActorId); + if (!ptr.isEmpty()) + { + // TODO: Show death animation before deleting? We shouldn't allow looting the corpse while the animation + // plays though, which is a rather lame exploit in vanilla. + MWBase::Environment::get().getWorld()->deleteObject(ptr); + + const ESM::Static* fx = MWBase::Environment::get().getWorld()->getStore().get() + .search("VFX_Summon_End"); + if (fx) + MWBase::Environment::get().getWorld()->spawnEffect("meshes\\" + fx->mModel, + "", Ogre::Vector3(ptr.getRefData().getPosition().pos)); + } + else + { + // We didn't find the creature. It's probably in an inactive cell. + // Add to graveyard so we can delete it when the cell becomes active. + std::vector& graveyard = casterStats.getSummonedCreatureGraveyard(); + graveyard.push_back(creatureActorId); + } +} + } namespace MWMechanics @@ -142,7 +221,7 @@ namespace MWMechanics // Use the smallest soulgem that is large enough to hold the soul MWWorld::ContainerStore& container = caster.getClass().getContainerStore(caster); MWWorld::ContainerStoreIterator gem = container.end(); - float gemCapacity = std::numeric_limits().max(); + float gemCapacity = std::numeric_limits::max(); std::string soulgemFilter = "misc_soulgem"; // no other way to check for soulgems? :/ for (MWWorld::ContainerStoreIterator it = container.begin(MWWorld::ContainerStore::Type_Miscellaneous); it != container.end(); ++it) @@ -170,6 +249,15 @@ namespace MWMechanics if (caster.getRefData().getHandle() == "player") MWBase::Environment::get().getWindowManager()->messageBox("#{sSoultrapSuccess}"); + + const ESM::Static* fx = MWBase::Environment::get().getWorld()->getStore().get() + .search("VFX_Soul_Trap"); + if (fx) + MWBase::Environment::get().getWorld()->spawnEffect("meshes\\" + fx->mModel, + "", Ogre::Vector3(mCreature.getRefData().getPosition().pos)); + + MWBase::Environment::get().getSoundManager()->playSound3D(mCreature, "conjuration hit", 1.f, 1.f, + MWBase::SoundManager::Play_TypeSfx, MWBase::SoundManager::Play_NoTrack); } }; @@ -182,62 +270,97 @@ namespace MWMechanics calculateCreatureStatModifiers (ptr, duration); // fatigue restoration - calculateRestoration(ptr, duration, false); + calculateRestoration(ptr, duration); } void Actors::engageCombat (const MWWorld::Ptr& actor1, const MWWorld::Ptr& actor2, bool againstPlayer) { CreatureStats& creatureStats = actor1.getClass().getCreatureStats(actor1); - - if (againstPlayer && creatureStats.isHostile()) return; // already fighting against player + + if (actor2.getClass().getCreatureStats(actor2).isDead() + || actor1.getClass().getCreatureStats(actor1).isDead()) + return; + + const ESM::Position& actor1Pos = actor1.getRefData().getPosition(); + const ESM::Position& actor2Pos = actor2.getRefData().getPosition(); + float sqrDist = Ogre::Vector3(actor1Pos.pos).squaredDistance(Ogre::Vector3(actor2Pos.pos)); + if (sqrDist > 7168*7168) + return; // pure water creatures won't try to fight with the target on the ground // except that creature is already hostile - if ((againstPlayer || !creatureStats.isHostile()) + if ((againstPlayer || !creatureStats.getAiSequence().isInCombat()) && ((actor1.getClass().canSwim(actor1) && !actor1.getClass().canWalk(actor1) // pure water creature && !MWBase::Environment::get().getWorld()->isSwimming(actor2)) || (!actor1.getClass().canSwim(actor1) && MWBase::Environment::get().getWorld()->isSwimming(actor2)))) // creature can't swim to target return; - float fight; + bool aggressive; + + if (againstPlayer) + { + // followers with high fight should not engage in combat with the player (e.g. bm_bear_black_summon) + const std::list& followers = getActorsFollowing(actor2); + if (std::find(followers.begin(), followers.end(), actor1) != followers.end()) + return; - if (againstPlayer) - fight = actor1.getClass().getCreatureStats(actor1).getAiSetting(CreatureStats::AI_Fight).getModified(); + aggressive = MWBase::Environment::get().getMechanicsManager()->isAggressive(actor1, actor2); + } else { - fight = 0; - // if one of actors is creature then we should make a decision to start combat or not - // NOTE: function doesn't take into account combat between 2 creatures - if (!actor1.getClass().isNpc()) + aggressive = false; + + // Make guards fight aggressive creatures + if (!actor1.getClass().isNpc() && actor2.getClass().isClass(actor2, "Guard")) { - // if creature is hostile then it is necessarily to start combat - if (creatureStats.isHostile()) fight = 100; - else fight = creatureStats.getAiSetting(CreatureStats::AI_Fight).getModified(); + if (creatureStats.getAiSequence().isInCombat() && MWBase::Environment::get().getMechanicsManager()->isAggressive(actor1, actor2)) + aggressive = true; } } - ESM::Position actor1Pos = actor1.getRefData().getPosition(); - ESM::Position actor2Pos = actor2.getRefData().getPosition(); - float d = Ogre::Vector3(actor1Pos.pos).distance(Ogre::Vector3(actor2Pos.pos)); + // start combat if target actor is in combat with one of our followers + const std::list& followers = getActorsFollowing(actor1); + const CreatureStats& creatureStats2 = actor2.getClass().getCreatureStats(actor2); + for (std::list::const_iterator it = followers.begin(); it != followers.end(); ++it) + { + // need to check both ways since player doesn't use AI packages + if ((creatureStats2.getAiSequence().isInCombat(*it) + || it->getClass().getCreatureStats(*it).getAiSequence().isInCombat(actor2)) + && !creatureStats.getAiSequence().isInCombat(*it)) + aggressive = true; + } - if( (fight == 100 && d <= 5000) - || (fight >= 95 && d <= 3000) - || (fight >= 90 && d <= 2000) - || (fight >= 80 && d <= 1000)) + // start combat if target actor is in combat with someone we are following + for (std::list::const_iterator it = creatureStats.getAiSequence().begin(); it != creatureStats.getAiSequence().end(); ++it) { - if (againstPlayer || actor2.getClass().getCreatureStats(actor2).getAiSequence().canAddTarget(actor2Pos, d)) + if ((*it)->getTypeId() == MWMechanics::AiPackage::TypeIdFollow) { - bool LOS = MWBase::Environment::get().getWorld()->getLOS(actor1, actor2); + MWWorld::Ptr followTarget = dynamic_cast(*it)->getTarget(); + if (followTarget.isEmpty()) + continue; + + if (creatureStats.getAiSequence().isInCombat(followTarget)) + continue; + + // need to check both ways since player doesn't use AI packages + if (creatureStats2.getAiSequence().isInCombat(followTarget) + || followTarget.getClass().getCreatureStats(followTarget).getAiSequence().isInCombat(actor2)) + aggressive = true; + } + } + + if(aggressive) + { + bool LOS = MWBase::Environment::get().getWorld()->getLOS(actor1, actor2); - if (againstPlayer) LOS &= MWBase::Environment::get().getMechanicsManager()->awarenessCheck(actor2, actor1); + if (againstPlayer) LOS &= MWBase::Environment::get().getMechanicsManager()->awarenessCheck(actor2, actor1); - if (LOS) + if (LOS) + { + MWBase::Environment::get().getMechanicsManager()->startCombat(actor1, actor2); + if (!againstPlayer) // start combat between each other { - MWBase::Environment::get().getMechanicsManager()->startCombat(actor1, actor2); - if (!againstPlayer) // start combat between each other - { - MWBase::Environment::get().getMechanicsManager()->startCombat(actor2, actor1); - } + MWBase::Environment::get().getMechanicsManager()->startCombat(actor2, actor1); } } } @@ -246,13 +369,15 @@ namespace MWMechanics void Actors::updateNpc (const MWWorld::Ptr& ptr, float duration) { updateDrowning(ptr, duration); - calculateNpcStatModifiers(ptr); + calculateNpcStatModifiers(ptr, duration); updateEquippedLight(ptr, duration); } void Actors::adjustMagicEffects (const MWWorld::Ptr& creature) { CreatureStats& creatureStats = creature.getClass().getCreatureStats (creature); + if (creatureStats.isDead()) + return; MagicEffects now = creatureStats.getSpells().getMagicEffects(); @@ -264,28 +389,30 @@ namespace MWMechanics now += creatureStats.getActiveSpells().getMagicEffects(); - //MagicEffects diff = MagicEffects::diff (creatureStats.getMagicEffects(), now); - - creatureStats.setMagicEffects(now); - - // TODO apply diff to other stats + creatureStats.modifyMagicEffects(now); } void Actors::calculateDynamicStats (const MWWorld::Ptr& ptr) { CreatureStats& creatureStats = ptr.getClass().getCreatureStats (ptr); - int strength = creatureStats.getAttribute(ESM::Attribute::Strength).getBase(); - int intelligence = creatureStats.getAttribute(ESM::Attribute::Intelligence).getBase(); - int willpower = creatureStats.getAttribute(ESM::Attribute::Willpower).getBase(); - int agility = creatureStats.getAttribute(ESM::Attribute::Agility).getBase(); - int endurance = creatureStats.getAttribute(ESM::Attribute::Endurance).getBase(); + int strength = creatureStats.getAttribute(ESM::Attribute::Strength).getModified(); + int intelligence = creatureStats.getAttribute(ESM::Attribute::Intelligence).getModified(); + int willpower = creatureStats.getAttribute(ESM::Attribute::Willpower).getModified(); + int agility = creatureStats.getAttribute(ESM::Attribute::Agility).getModified(); + int endurance = creatureStats.getAttribute(ESM::Attribute::Endurance).getModified(); + + float base = 1.f; + if (ptr.getCellRef().getRefId() == "player") + base = MWBase::Environment::get().getWorld()->getStore().get().find("fPCbaseMagickaMult")->getFloat(); + else + base = MWBase::Environment::get().getWorld()->getStore().get().find("fNPCbaseMagickaMult")->getFloat(); - double magickaFactor = - creatureStats.getMagicEffects().get (EffectKey (ESM::MagicEffect::FortifyMaximumMagicka)).mMagnitude * 0.1 + 0.5; + double magickaFactor = base + + creatureStats.getMagicEffects().get (EffectKey (ESM::MagicEffect::FortifyMaximumMagicka)).getMagnitude() * 0.1; DynamicStat magicka = creatureStats.getMagicka(); - float diff = (static_cast(intelligence + magickaFactor*intelligence)) - magicka.getBase(); + float diff = (static_cast(magickaFactor*intelligence)) - magicka.getBase(); magicka.modify(diff); creatureStats.setMagicka(magicka); @@ -295,7 +422,7 @@ namespace MWMechanics creatureStats.setFatigue(fatigue); } - void Actors::calculateRestoration (const MWWorld::Ptr& ptr, float duration, bool sleep) + void Actors::restoreDynamicStats (const MWWorld::Ptr& ptr, bool sleep) { if (ptr.getClass().getCreatureStats(ptr).isDead()) return; @@ -319,9 +446,7 @@ namespace MWMechanics int endurance = stats.getAttribute (ESM::Attribute::Endurance).getModified (); - float capacity = ptr.getClass().getCapacity(ptr); - float encumbrance = ptr.getClass().getEncumbrance(ptr); - float normalizedEncumbrance = (capacity == 0 ? 1 : encumbrance/capacity); + float normalizedEncumbrance = ptr.getClass().getNormalizedEncumbrance(ptr); if (normalizedEncumbrance > 1) normalizedEncumbrance = 1; @@ -334,9 +459,29 @@ namespace MWMechanics x *= fEndFatigueMult * endurance; DynamicStat fatigue = stats.getFatigue(); - fatigue.setCurrent (fatigue.getCurrent() + duration * x); + fatigue.setCurrent (fatigue.getCurrent() + 3600 * x); stats.setFatigue (fatigue); + } + + void Actors::calculateRestoration (const MWWorld::Ptr& ptr, float duration) + { + if (ptr.getClass().getCreatureStats(ptr).isDead()) + return; + + MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats (ptr); + int endurance = stats.getAttribute (ESM::Attribute::Endurance).getModified (); + + // restore fatigue + const MWWorld::Store& settings = MWBase::Environment::get().getWorld()->getStore().get(); + static const float fFatigueReturnBase = settings.find("fFatigueReturnBase")->getFloat (); + static const float fFatigueReturnMult = settings.find("fFatigueReturnMult")->getFloat (); + + float x = fFatigueReturnBase + fFatigueReturnMult * endurance; + + DynamicStat fatigue = stats.getFatigue(); + fatigue.setCurrent (fatigue.getCurrent() + duration * x); + stats.setFatigue (fatigue); } void Actors::calculateCreatureStatModifiers (const MWWorld::Ptr& ptr, float duration) @@ -344,28 +489,50 @@ namespace MWMechanics CreatureStats &creatureStats = ptr.getClass().getCreatureStats(ptr); const MagicEffects &effects = creatureStats.getMagicEffects(); + bool wasDead = creatureStats.isDead(); + // attributes for(int i = 0;i < ESM::Attribute::Length;++i) { AttributeValue stat = creatureStats.getAttribute(i); - stat.setModifier(effects.get(EffectKey(ESM::MagicEffect::FortifyAttribute, i)).mMagnitude - - effects.get(EffectKey(ESM::MagicEffect::DrainAttribute, i)).mMagnitude - - effects.get(EffectKey(ESM::MagicEffect::AbsorbAttribute, i)).mMagnitude); + stat.setModifier(effects.get(EffectKey(ESM::MagicEffect::FortifyAttribute, i)).getMagnitude() - + effects.get(EffectKey(ESM::MagicEffect::DrainAttribute, i)).getMagnitude() - + effects.get(EffectKey(ESM::MagicEffect::AbsorbAttribute, i)).getMagnitude()); + + stat.damage(effects.get(EffectKey(ESM::MagicEffect::DamageAttribute, i)).getMagnitude() * duration * 1.5); + stat.restore(effects.get(EffectKey(ESM::MagicEffect::RestoreAttribute, i)).getMagnitude() * duration * 1.5); creatureStats.setAttribute(i, stat); } + { + Spells & spells = creatureStats.getSpells(); + for (Spells::TIterator it = spells.begin(); it != spells.end(); ++it) + { + if (spells.getCorprusSpells().find(it->first) != spells.getCorprusSpells().end()) + { + if (MWBase::Environment::get().getWorld()->getTimeStamp() >= spells.getCorprusSpells().at(it->first).mNextWorsening) + { + spells.worsenCorprus(it->first); + + if (ptr.getRefData().getHandle() == "player") + MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicCorprusWorsens}"); + } + } + } + } + // dynamic stats for(int i = 0;i < 3;++i) { DynamicStat stat = creatureStats.getDynamic(i); - stat.setModifier(effects.get(ESM::MagicEffect::FortifyHealth+i).mMagnitude - - effects.get(ESM::MagicEffect::DrainHealth+i).mMagnitude); + stat.setModifier(effects.get(ESM::MagicEffect::FortifyHealth+i).getMagnitude() - + effects.get(ESM::MagicEffect::DrainHealth+i).getMagnitude()); - float currentDiff = creatureStats.getMagicEffects().get(ESM::MagicEffect::RestoreHealth+i).mMagnitude - - creatureStats.getMagicEffects().get(ESM::MagicEffect::DamageHealth+i).mMagnitude - - creatureStats.getMagicEffects().get(ESM::MagicEffect::AbsorbHealth+i).mMagnitude; + float currentDiff = creatureStats.getMagicEffects().get(ESM::MagicEffect::RestoreHealth+i).getMagnitude() + - creatureStats.getMagicEffects().get(ESM::MagicEffect::DamageHealth+i).getMagnitude() + - creatureStats.getMagicEffects().get(ESM::MagicEffect::AbsorbHealth+i).getMagnitude(); stat.setCurrent(stat.getCurrent() + currentDiff * duration, i == 2); creatureStats.setDynamic(i, stat); @@ -379,27 +546,27 @@ namespace MWMechanics if (!creature || ptr.get()->mBase->mData.mType == ESM::Creature::Creatures) { Stat stat = creatureStats.getAiSetting(CreatureStats::AI_Fight); - stat.setModifier(creatureStats.getMagicEffects().get(ESM::MagicEffect::FrenzyHumanoid+creature).mMagnitude - - creatureStats.getMagicEffects().get(ESM::MagicEffect::CalmHumanoid+creature).mMagnitude); + stat.setModifier(creatureStats.getMagicEffects().get(ESM::MagicEffect::FrenzyHumanoid+creature).getMagnitude() + - creatureStats.getMagicEffects().get(ESM::MagicEffect::CalmHumanoid+creature).getMagnitude()); creatureStats.setAiSetting(CreatureStats::AI_Fight, stat); stat = creatureStats.getAiSetting(CreatureStats::AI_Flee); - stat.setModifier(creatureStats.getMagicEffects().get(ESM::MagicEffect::DemoralizeHumanoid+creature).mMagnitude - - creatureStats.getMagicEffects().get(ESM::MagicEffect::RallyHumanoid+creature).mMagnitude); + stat.setModifier(creatureStats.getMagicEffects().get(ESM::MagicEffect::DemoralizeHumanoid+creature).getMagnitude() + - creatureStats.getMagicEffects().get(ESM::MagicEffect::RallyHumanoid+creature).getMagnitude()); creatureStats.setAiSetting(CreatureStats::AI_Flee, stat); } if (creature && ptr.get()->mBase->mData.mType == ESM::Creature::Undead) { Stat stat = creatureStats.getAiSetting(CreatureStats::AI_Flee); - stat.setModifier(creatureStats.getMagicEffects().get(ESM::MagicEffect::TurnUndead).mMagnitude); + stat.setModifier(creatureStats.getMagicEffects().get(ESM::MagicEffect::TurnUndead).getMagnitude()); creatureStats.setAiSetting(CreatureStats::AI_Flee, stat); } // Apply disintegration (reduces item health) - float disintegrateWeapon = effects.get(ESM::MagicEffect::DisintegrateWeapon).mMagnitude; + float disintegrateWeapon = effects.get(ESM::MagicEffect::DisintegrateWeapon).getMagnitude(); if (disintegrateWeapon > 0) disintegrateSlot(ptr, MWWorld::InventoryStore::Slot_CarriedRight, disintegrateWeapon*duration); - float disintegrateArmor = effects.get(ESM::MagicEffect::DisintegrateArmor).mMagnitude; + float disintegrateArmor = effects.get(ESM::MagicEffect::DisintegrateArmor).getMagnitude(); if (disintegrateArmor > 0) { // According to UESP @@ -422,6 +589,12 @@ namespace MWMechanics } } + bool receivedMagicDamage = false; + + if (creatureStats.getMagicEffects().get(ESM::MagicEffect::DamageHealth).getMagnitude() > 0.0f + || creatureStats.getMagicEffects().get(ESM::MagicEffect::AbsorbHealth).getMagnitude() > 0.0f) + receivedMagicDamage = true; + // Apply damage ticks int damageEffects[] = { ESM::MagicEffect::FireDamage, ESM::MagicEffect::ShockDamage, ESM::MagicEffect::FrostDamage, ESM::MagicEffect::Poison, @@ -431,7 +604,7 @@ namespace MWMechanics DynamicStat health = creatureStats.getHealth(); for (unsigned int i=0; i 1) damageScale *= fMagicSunBlockedMult; health.setCurrent(health.getCurrent() - magnitude * duration * damageScale); + + if (magnitude * damageScale > 0.0f) + receivedMagicDamage = true; } else + { health.setCurrent(health.getCurrent() - magnitude * duration); + if (magnitude > 0.0f) + receivedMagicDamage = true; + } } + + if (receivedMagicDamage && ptr.getRefData().getHandle() == "player") + MWBase::Environment::get().getWindowManager()->activateHitOverlay(false); + creatureStats.setHealth(health); + if (!wasDead && creatureStats.isDead()) + { + // The actor was killed by a magic effect. Figure out if the player was responsible for it. + const ActiveSpells& spells = creatureStats.getActiveSpells(); + bool killedByPlayer = false; + bool murderedByPlayer = false; + MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); + for (ActiveSpells::TIterator it = spells.begin(); it != spells.end(); ++it) + { + const ActiveSpells::ActiveSpellParams& spell = it->second; + for (std::vector::const_iterator effectIt = spell.mEffects.begin(); + effectIt != spell.mEffects.end(); ++effectIt) + { + int effectId = effectIt->mEffectId; + bool isDamageEffect = false; + for (unsigned int i=0; isearchPtrViaActorId(spell.mCasterActorId); + if (isDamageEffect && caster == player) + { + killedByPlayer = true; + // Simple check for who attacked first: if the player attacked first, a crimeId should be set + // Doesn't handle possible edge case where no one reported the assault, but in such a case, + // for bystanders it is not possible to tell who attacked first, anyway. + if (ptr.getClass().isNpc() && ptr.getClass().getNpcStats(ptr).getCrimeId() != -1 + && ptr != player) + murderedByPlayer = true; + } + } + } + if (murderedByPlayer) + MWBase::Environment::get().getMechanicsManager()->commitCrime(player, ptr, MWBase::MechanicsManager::OT_Murder); + if (killedByPlayer && player.getClass().getNpcStats(player).isWerewolf()) + player.getClass().getNpcStats(player).addWerewolfKill(); + } + // TODO: dirty flag for magic effects to avoid some unnecessary work below? // Update bound effects @@ -478,7 +706,7 @@ namespace MWMechanics for (std::map::iterator it = boundItemsMap.begin(); it != boundItemsMap.end(); ++it) { bool found = creatureStats.mBoundItems.find(it->first) != creatureStats.mBoundItems.end(); - int magnitude = creatureStats.getMagicEffects().get(it->first).mMagnitude; + int magnitude = creatureStats.getMagicEffects().get(it->first).getMagnitude(); if (found != (magnitude > 0)) { std::string itemGmst = it->second; @@ -486,8 +714,12 @@ namespace MWMechanics itemGmst)->getString(); if (it->first == ESM::MagicEffect::BoundGloves) { - adjustBoundItem("sMagicBoundLeftGauntletID", magnitude > 0, ptr); - adjustBoundItem("sMagicBoundRightGauntletID", magnitude > 0, ptr); + item = MWBase::Environment::get().getWorld()->getStore().get().find( + "sMagicBoundLeftGauntletID")->getString(); + adjustBoundItem(item, magnitude > 0, ptr); + item = MWBase::Environment::get().getWorld()->getStore().get().find( + "sMagicBoundRightGauntletID")->getString(); + adjustBoundItem(item, magnitude > 0, ptr); } else adjustBoundItem(item, magnitude > 0, ptr); @@ -527,10 +759,11 @@ namespace MWMechanics summonMap[ESM::MagicEffect::SummonCreature05] = "sMagicCreature05ID"; } + std::map& creatureMap = creatureStats.getSummonedCreatureMap(); for (std::map::iterator it = summonMap.begin(); it != summonMap.end(); ++it) { - bool found = creatureStats.mSummonedCreatures.find(it->first) != creatureStats.mSummonedCreatures.end(); - int magnitude = creatureStats.getMagicEffects().get(it->first).mMagnitude; + bool found = creatureMap.find(it->first) != creatureMap.end(); + int magnitude = creatureStats.getMagicEffects().get(it->first).getMagnitude(); if (found != (magnitude > 0)) { if (magnitude > 0) @@ -559,46 +792,65 @@ namespace MWMechanics MWMechanics::CreatureStats& summonedCreatureStats = ref.getPtr().getClass().getCreatureStats(ref.getPtr()); // Make the summoned creature follow its master and help in fights - AiFollow package(ptr.getRefData().getHandle()); + AiFollow package(ptr.getCellRef().getRefId()); summonedCreatureStats.getAiSequence().stack(package, ref.getPtr()); int creatureActorId = summonedCreatureStats.getActorId(); - MWBase::Environment::get().getWorld()->safePlaceObject(ref.getPtr(),store,ipos); + MWWorld::Ptr placed = MWBase::Environment::get().getWorld()->safePlaceObject(ref.getPtr(),store,ipos); - // TODO: VFX_SummonStart, VFX_SummonEnd - creatureStats.mSummonedCreatures.insert(std::make_pair(it->first, creatureActorId)); + MWRender::Animation* anim = MWBase::Environment::get().getWorld()->getAnimation(placed); + if (anim) + { + const ESM::Static* fx = MWBase::Environment::get().getWorld()->getStore().get() + .search("VFX_Summon_Start"); + if (fx) + anim->addEffect("meshes\\" + fx->mModel, -1, false); + } + + creatureMap.insert(std::make_pair(it->first, creatureActorId)); } } else { - // Summon lifetime has expired. Try to delete the creature. - int actorId = creatureStats.mSummonedCreatures[it->first]; - creatureStats.mSummonedCreatures.erase(it->first); - - MWWorld::Ptr ptr = MWBase::Environment::get().getWorld()->searchPtrViaActorId(actorId); - if (!ptr.isEmpty()) - { - // TODO: Show death animation before deleting? We shouldn't allow looting the corpse while the animation - // plays though, which is a rather lame exploit in vanilla. - MWBase::Environment::get().getWorld()->deleteObject(ptr); - creatureStats.mSummonedCreatures.erase(it->first); - } - else - { - // We didn't find the creature. It's probably in an inactive cell. - // Add to graveyard so we can delete it when the cell becomes active. - creatureStats.mSummonGraveyard.push_back(actorId); - } + // Effect has ended + std::map::iterator foundCreature = creatureMap.find(it->first); + cleanupSummonedCreature(creatureStats, foundCreature->second); + creatureMap.erase(foundCreature); } } } - for (std::vector::iterator it = creatureStats.mSummonGraveyard.begin(); it != creatureStats.mSummonGraveyard.end(); ) + for (std::map::iterator it = creatureMap.begin(); it != creatureMap.end(); ) + { + MWWorld::Ptr ptr = MWBase::Environment::get().getWorld()->searchPtrViaActorId(it->second); + if (!ptr.isEmpty() && ptr.getClass().getCreatureStats(ptr).isDead()) + { + // Purge the magic effect so a new creature can be summoned if desired + creatureStats.getActiveSpells().purgeEffect(it->first); + if (ptr.getClass().hasInventoryStore(ptr)) + ptr.getClass().getInventoryStore(ptr).purgeEffect(it->first); + + cleanupSummonedCreature(creatureStats, it->second); + creatureMap.erase(it++); + } + else + ++it; + } + + std::vector& graveyard = creatureStats.getSummonedCreatureGraveyard(); + for (std::vector::iterator it = graveyard.begin(); it != graveyard.end(); ) { MWWorld::Ptr ptr = MWBase::Environment::get().getWorld()->searchPtrViaActorId(*it); if (!ptr.isEmpty()) { - it = creatureStats.mSummonGraveyard.erase(it); + it = graveyard.erase(it); + + const ESM::Static* fx = MWBase::Environment::get().getWorld()->getStore().get() + .search("VFX_Summon_End"); + if (fx) + MWBase::Environment::get().getWorld()->spawnEffect("meshes\\" + fx->mModel, + "", Ogre::Vector3(ptr.getRefData().getPosition().pos)); + MWBase::Environment::get().getWorld()->deleteObject(ptr); } else @@ -606,7 +858,7 @@ namespace MWMechanics } } - void Actors::calculateNpcStatModifiers (const MWWorld::Ptr& ptr) + void Actors::calculateNpcStatModifiers (const MWWorld::Ptr& ptr, float duration) { NpcStats &npcStats = ptr.getClass().getNpcStats(ptr); const MagicEffects &effects = npcStats.getMagicEffects(); @@ -615,9 +867,12 @@ namespace MWMechanics for(int i = 0;i < ESM::Skill::Length;++i) { SkillValue& skill = npcStats.getSkill(i); - skill.setModifier(effects.get(EffectKey(ESM::MagicEffect::FortifySkill, i)).mMagnitude - - effects.get(EffectKey(ESM::MagicEffect::DrainSkill, i)).mMagnitude - - effects.get(EffectKey(ESM::MagicEffect::AbsorbSkill, i)).mMagnitude); + skill.setModifier(effects.get(EffectKey(ESM::MagicEffect::FortifySkill, i)).getMagnitude() - + effects.get(EffectKey(ESM::MagicEffect::DrainSkill, i)).getMagnitude() - + effects.get(EffectKey(ESM::MagicEffect::AbsorbSkill, i)).getMagnitude()); + + skill.damage(effects.get(EffectKey(ESM::MagicEffect::DamageSkill, i)).getMagnitude() * duration * 1.5); + skill.restore(effects.get(EffectKey(ESM::MagicEffect::RestoreSkill, i)).getMagnitude() * duration * 1.5); } } @@ -626,7 +881,7 @@ namespace MWMechanics MWBase::World *world = MWBase::Environment::get().getWorld(); NpcStats &stats = ptr.getClass().getNpcStats(ptr); if(world->isSubmerged(ptr) && - stats.getMagicEffects().get(ESM::MagicEffect::WaterBreathing).mMagnitude == 0) + stats.getMagicEffects().get(ESM::MagicEffect::WaterBreathing).getMagnitude() == 0) { float timeLeft = 0.0f; if(stats.getFatigue().getCurrent() == 0) @@ -644,13 +899,13 @@ namespace MWMechanics static const float fSuffocationDamage = world->getStore().get().find("fSuffocationDamage")->getFloat(); ptr.getClass().setActorHealth(ptr, stats.getHealth().getCurrent() - fSuffocationDamage*duration); - // Play a drowning sound as necessary for the player + // Play a drowning sound + MWBase::SoundManager *sndmgr = MWBase::Environment::get().getSoundManager(); + if(!sndmgr->getSoundPlaying(ptr, "drown")) + sndmgr->playSound3D(ptr, "drown", 1.0f, 1.0f); + if(ptr == world->getPlayerPtr()) - { - MWBase::SoundManager *sndmgr = MWBase::Environment::get().getSoundManager(); - if(!sndmgr->getSoundPlaying(MWWorld::Ptr(), "drown")) - sndmgr->playSound("drown", 1.0f, 1.0f); - } + MWBase::Environment::get().getWindowManager()->activateHitOverlay(false); } } else @@ -686,7 +941,7 @@ namespace MWMechanics { if (torch != inventoryStore.end()) { - if (!ptr.getClass().getCreatureStats (ptr).isHostile()) + if (!ptr.getClass().getCreatureStats (ptr).getAiSequence().isInCombat()) { // For non-hostile NPCs, unequip whatever is in the left slot in favor of a light. if (heldIter != inventoryStore.end() && heldIter->getTypeName() != typeid(ESM::Light).name()) @@ -766,11 +1021,13 @@ namespace MWMechanics CreatureStats& creatureStats = ptr.getClass().getCreatureStats(ptr); NpcStats& npcStats = ptr.getClass().getNpcStats(ptr); - if (ptr.getClass().isClass(ptr, "Guard") && creatureStats.getAiSequence().getTypeId() != AiPackage::TypeIdPursue && !creatureStats.isHostile()) + if (player.getClass().getNpcStats(player).isWerewolf()) + return; + + if (ptr.getClass().isClass(ptr, "Guard") && creatureStats.getAiSequence().getTypeId() != AiPackage::TypeIdPursue && !creatureStats.getAiSequence().isInCombat()) { - /// \todo Move me! I shouldn't be here... const MWWorld::ESMStore& esmStore = MWBase::Environment::get().getWorld()->getStore(); - float cutoff = float(esmStore.get().find("iCrimeThreshold")->getInt()); + int cutoff = esmStore.get().find("iCrimeThreshold")->getInt(); // Force dialogue on sight if bounty is greater than the cutoff // In vanilla morrowind, the greeting dialogue is scripted to either arrest the player (< 5000 bounty) or attack (>= 5000 bounty) if ( player.getClass().getNpcStats(player).getBounty() >= cutoff @@ -778,7 +1035,11 @@ namespace MWMechanics && MWBase::Environment::get().getWorld()->getLOS(ptr, player) && MWBase::Environment::get().getMechanicsManager()->awarenessCheck(player, ptr)) { - creatureStats.getAiSequence().stack(AiPursue(player), ptr); + static int iCrimeThresholdMultiplier = esmStore.get().find("iCrimeThresholdMultiplier")->getInt(); + if (player.getClass().getNpcStats(player).getBounty() >= cutoff * iCrimeThresholdMultiplier) + MWBase::Environment::get().getMechanicsManager()->startCombat(ptr, player); + else + creatureStats.getAiSequence().stack(AiPursue(player), ptr); creatureStats.setAlarmed(true); npcStats.setCrimeId(MWBase::Environment::get().getWorld()->getPlayer().getNewCrimeId()); } @@ -796,23 +1057,12 @@ namespace MWMechanics creatureStats.getAiSequence().stopCombat(); // Reset factors to attack - // TODO: Not a complete list, disposition changes? - creatureStats.setHostile(false); creatureStats.setAttacked(false); creatureStats.setAlarmed(false); // Update witness crime id npcStats.setCrimeId(-1); } - else if (!creatureStats.isHostile() && creatureStats.getAiSequence().getTypeId() != AiPackage::TypeIdPursue) - { - if (ptr.getClass().isClass(ptr, "Guard")) - creatureStats.getAiSequence().stack(AiPursue(player), ptr); - else - { - MWBase::Environment::get().getMechanicsManager()->startCombat(ptr, player); - } - } } } } @@ -821,19 +1071,11 @@ namespace MWMechanics Actors::~Actors() { - PtrControllerMap::iterator it(mActors.begin()); - for (; it != mActors.end(); ++it) - { - delete it->second; - it->second = NULL; - } + clear(); } void Actors::addActor (const MWWorld::Ptr& ptr, bool updateImmediately) { - // erase previous death events since we are currently only tracking them while in an active cell - ptr.getClass().getCreatureStats(ptr).clearHasDied(); - removeActor(ptr); MWRender::Animation *anim = MWBase::Environment::get().getWorld()->getAnimation(ptr); @@ -880,19 +1122,10 @@ namespace MWMechanics } } - static Ogre::Vector3 sBasePoint; - bool comparePtrDist (const MWWorld::Ptr& ptr1, const MWWorld::Ptr& ptr2) - { - return (sBasePoint.squaredDistance(Ogre::Vector3(ptr1.getRefData().getPosition().pos)) - < sBasePoint.squaredDistance(Ogre::Vector3(ptr2.getRefData().getPosition().pos))); - } - void Actors::update (float duration, bool paused) { if(!paused) { - std::list listGuards; // at the moment only guards certainly will fight with creatures - static float timerUpdateAITargets = 0; // target lists get updated once every 1.0 sec @@ -905,15 +1138,17 @@ namespace MWMechanics // Note, the new hit object for this frame may be set by CharacterController::update -> Animation::runAnimation // (below) iter->first.getClass().getCreatureStats(iter->first).setLastHitObject(std::string()); - - // add guards to list to later make them fight with creatures - if (timerUpdateAITargets == 0 && iter->first.getClass().isClass(iter->first, "Guard")) - listGuards.push_back(iter->first); } MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); - listGuards.push_back(player); + int hostilesCount = 0; // need to know this to play Battle music + + // AI processing is only done within distance of 7168 units to the player. Note the "AI distance" slider doesn't affect this + // (it only does some throttling for targets beyond the "AI distance", so doesn't give any guarantees as to whether AI will be enabled or not) + // This distance could be made configurable later, but the setting must be marked with a big warning: + // using higher values will make a quest in Bloodmoon harder or impossible to complete (bug #1876) + const float sqrProcessingDistance = 7168*7168; // AI and magic effects update for(PtrControllerMap::iterator iter(mActors.begin()); iter != mActors.end(); ++iter) @@ -921,28 +1156,34 @@ namespace MWMechanics if (!iter->first.getClass().getCreatureStats(iter->first).isDead()) { updateActor(iter->first, duration); - - if (MWBase::Environment::get().getMechanicsManager()->isAIActive()) + if (MWBase::Environment::get().getMechanicsManager()->isAIActive() && + Ogre::Vector3(player.getRefData().getPosition().pos).squaredDistance(Ogre::Vector3(iter->first.getRefData().getPosition().pos)) + <= sqrProcessingDistance) { - // make guards and creatures fight each other - if (timerUpdateAITargets == 0 && iter->first.getTypeName() == typeid(ESM::Creature).name() && !listGuards.empty()) + if (timerUpdateAITargets == 0) { - sBasePoint = Ogre::Vector3(iter->first.getRefData().getPosition().pos); - listGuards.sort(comparePtrDist); // try to engage combat starting from the nearest creature - - for (std::list::iterator it = listGuards.begin(); it != listGuards.end(); ++it) + if (iter->first != player) + adjustCommandedActor(iter->first); + + for(PtrControllerMap::iterator it(mActors.begin()); it != mActors.end(); ++it) { - engageCombat(iter->first, *it, false); + if (it->first == iter->first || iter->first == player) // player is not AI-controlled + continue; + engageCombat(iter->first, it->first, it->first == player); } } - if (iter->first != player) engageCombat(iter->first, player, true); - if (iter->first.getClass().isNpc() && iter->first != player) updateCrimePersuit(iter->first, duration); if (iter->first != player) - iter->first.getClass().getCreatureStats(iter->first).getAiSequence().execute(iter->first, duration); + iter->first.getClass().getCreatureStats(iter->first).getAiSequence().execute(iter->first,iter->second->getAiState(), duration); + + CreatureStats &stats = iter->first.getClass().getCreatureStats(iter->first); + if(!stats.isDead()) + { + if (stats.getAiSequence().isInCombat()) hostilesCount++; + } } if(iter->first.getTypeName() == typeid(ESM::NPC).name()) @@ -961,15 +1202,31 @@ namespace MWMechanics iter->second->updateContinuousVfx(); // Animation/movement update + CharacterController* playerCharacter = NULL; for(PtrControllerMap::iterator iter(mActors.begin()); iter != mActors.end(); ++iter) { + if (iter->first != player && + Ogre::Vector3(player.getRefData().getPosition().pos).squaredDistance(Ogre::Vector3(iter->first.getRefData().getPosition().pos)) + > sqrProcessingDistance) + continue; + if (iter->first.getClass().getCreatureStats(iter->first).getMagicEffects().get( - ESM::MagicEffect::Paralyze).mMagnitude > 0) + ESM::MagicEffect::Paralyze).getMagnitude() > 0) iter->second->skipAnim(); + + // Handle player last, in case a cell transition occurs by casting a teleportation spell + // (would invalidate the iterator) + if (iter->first.getCellRef().getRefId() == "player") + { + playerCharacter = iter->second; + continue; + } iter->second->update(duration); } - // Kill dead actors, update some variables + if (playerCharacter) + playerCharacter->update(duration); + for(PtrControllerMap::iterator iter(mActors.begin()); iter != mActors.end(); ++iter) { const MWWorld::Class &cls = iter->first.getClass(); @@ -978,103 +1235,155 @@ namespace MWMechanics //KnockedOutOneFrameLogic //Used for "OnKnockedOut" command //Put here to ensure that it's run for PRECISELY one frame. - if (stats.getKnockedDown() && !stats.getKnockedDownOneFrame() && !stats.getKnockedDownOverOneFrame()) { //Start it for one frame if nessesary + if (stats.getKnockedDown() && !stats.getKnockedDownOneFrame() && !stats.getKnockedDownOverOneFrame()) + { //Start it for one frame if nessesary stats.setKnockedDownOneFrame(true); } - else if (stats.getKnockedDownOneFrame() && !stats.getKnockedDownOverOneFrame()) { //Turn off KnockedOutOneframe + else if (stats.getKnockedDownOneFrame() && !stats.getKnockedDownOverOneFrame()) + { //Turn off KnockedOutOneframe stats.setKnockedDownOneFrame(false); stats.setKnockedDownOverOneFrame(true); } + } - if(!stats.isDead()) - { - if(iter->second->isDead()) - iter->second->resurrect(); + killDeadActors(); - if(!stats.isDead()) - continue; - } + // check if we still have any player enemies to switch music + static bool isBattleMusic = false; - // If it's the player and God Mode is turned on, keep it alive - if (iter->first.getRefData().getHandle()=="player" && - MWBase::Environment::get().getWorld()->getGodModeState()) - { - MWMechanics::DynamicStat stat (stats.getHealth()); + if (isBattleMusic && hostilesCount == 0 && !(player.getClass().getCreatureStats(player).isDead() && + MWBase::Environment::get().getSoundManager()->isMusicPlaying())) + { + MWBase::Environment::get().getSoundManager()->playPlaylist(std::string("Explore")); + isBattleMusic = false; + } + else if (!isBattleMusic && hostilesCount > 0) + { + MWBase::Environment::get().getSoundManager()->playPlaylist(std::string("Battle")); + isBattleMusic = true; + } - if (stat.getModified()<1) - { - stat.setModified(1, 0); - stats.setHealth(stat); - } - stats.resurrect(); - continue; - } + static float sneakTimer = 0.f; // times update of sneak icon - if (iter->second->kill()) + // if player is in sneak state see if anyone detects him + if (player.getClass().getCreatureStats(player).getMovementFlag(MWMechanics::CreatureStats::Flag_Sneak)) + { + static float sneakSkillTimer = 0.f; // times sneak skill progress from "avoid notice" + + const MWWorld::ESMStore& esmStore = MWBase::Environment::get().getWorld()->getStore(); + const int radius = esmStore.get().find("fSneakUseDist")->getInt(); + + static float fSneakUseDelay = esmStore.get().find("fSneakUseDelay")->getFloat(); + + if (sneakTimer >= fSneakUseDelay) + sneakTimer = 0.f; + + if (sneakTimer == 0.f) { - ++mDeathCount[cls.getId(iter->first)]; + // Set when an NPC is within line of sight and distance, but is still unaware. Used for skill progress. + bool avoidedNotice = false; - // Make sure spell effects with CasterLinked flag are removed - for (PtrControllerMap::iterator iter2(mActors.begin());iter2 != mActors.end();++iter2) - { - MWMechanics::ActiveSpells& spells = iter2->first.getClass().getCreatureStats(iter2->first).getActiveSpells(); - spells.purge(stats.getActorId()); - } + bool detected = false; - // Apply soultrap - if (iter->first.getTypeName() == typeid(ESM::Creature).name()) + for (PtrControllerMap::iterator iter(mActors.begin()); iter != mActors.end(); ++iter) { - SoulTrap soulTrap (iter->first); - stats.getActiveSpells().visitEffectSources(soulTrap); + if (iter->first == player) // not the player + continue; + + // is the player in range and can they be detected + if (Ogre::Vector3(iter->first.getRefData().getPosition().pos).squaredDistance(Ogre::Vector3(player.getRefData().getPosition().pos)) <= radius*radius + && MWBase::Environment::get().getWorld()->getLOS(player, iter->first)) + { + if (MWBase::Environment::get().getMechanicsManager()->awarenessCheck(player, iter->first)) + { + detected = true; + avoidedNotice = false; + MWBase::Environment::get().getWindowManager()->setSneakVisibility(false); + break; + } + else if (!detected) + avoidedNotice = true; + } } - // Reset magic effects and recalculate derived effects - // One case where we need this is to make sure bound items are removed upon death - stats.setMagicEffects(MWMechanics::MagicEffects()); - stats.getActiveSpells().clear(); - calculateCreatureStatModifiers(iter->first, 0); + if (sneakSkillTimer >= fSneakUseDelay) + sneakSkillTimer = 0.f; - MWBase::Environment::get().getWorld()->enableActorCollision(iter->first, false); + if (avoidedNotice && sneakSkillTimer == 0.f) + player.getClass().skillUsageSucceeded(player, ESM::Skill::Sneak, 0); - if (cls.isEssential(iter->first)) - MWBase::Environment::get().getWindowManager()->messageBox("#{sKilledEssential}"); + if (!detected) + MWBase::Environment::get().getWindowManager()->setSneakVisibility(true); } + sneakTimer += duration; + sneakSkillTimer += duration; } + else + { + sneakTimer = 0.f; + MWBase::Environment::get().getWindowManager()->setSneakVisibility(false); + } + } + } - // if player is in sneak state see if anyone detects him - if (player.getClass().getCreatureStats(player).getMovementFlag(MWMechanics::CreatureStats::Flag_Sneak)) + void Actors::killDeadActors() + { + for(PtrControllerMap::iterator iter(mActors.begin()); iter != mActors.end(); ++iter) + { + const MWWorld::Class &cls = iter->first.getClass(); + CreatureStats &stats = cls.getCreatureStats(iter->first); + + if(!stats.isDead()) { - const MWWorld::ESMStore& esmStore = MWBase::Environment::get().getWorld()->getStore(); - const int radius = esmStore.get().find("fSneakUseDist")->getInt(); - bool detected = false; + if(iter->second->isDead()) + { + // Actor has been resurrected. Notify the CharacterController and re-enable collision. + MWBase::Environment::get().getWorld()->enableActorCollision(iter->first, true); + iter->second->resurrect(); + } + + if(!stats.isDead()) + continue; + } + + if (iter->second->kill()) + { + iter->first.getClass().getCreatureStats(iter->first).notifyDied(); + + ++mDeathCount[Misc::StringUtils::lowerCase(iter->first.getCellRef().getRefId())]; - for (PtrControllerMap::iterator iter(mActors.begin()); iter != mActors.end(); ++iter) + // Make sure spell effects with CasterLinked flag are removed + for (PtrControllerMap::iterator iter2(mActors.begin());iter2 != mActors.end();++iter2) { - if (iter->first == player) // not the player - continue; + MWMechanics::ActiveSpells& spells = iter2->first.getClass().getCreatureStats(iter2->first).getActiveSpells(); + spells.purge(stats.getActorId()); + } - // is the player in range and can they be detected - if ( (Ogre::Vector3(iter->first.getRefData().getPosition().pos).squaredDistance(Ogre::Vector3(player.getRefData().getPosition().pos)) <= radius*radius) - && MWBase::Environment::get().getMechanicsManager()->awarenessCheck(player, iter->first) - && MWBase::Environment::get().getWorld()->getLOS(player, iter->first)) - { - detected = true; - MWBase::Environment::get().getWindowManager()->setSneakVisibility(false); - break; - } + // Apply soultrap + if (iter->first.getTypeName() == typeid(ESM::Creature).name()) + { + SoulTrap soulTrap (iter->first); + stats.getActiveSpells().visitEffectSources(soulTrap); } - if (!detected) - MWBase::Environment::get().getWindowManager()->setSneakVisibility(true); + // Reset magic effects and recalculate derived effects + // One case where we need this is to make sure bound items are removed upon death + stats.modifyMagicEffects(MWMechanics::MagicEffects()); + stats.getActiveSpells().clear(); + calculateCreatureStatModifiers(iter->first, 0); + + MWBase::Environment::get().getWorld()->enableActorCollision(iter->first, false); + + if (cls.isEssential(iter->first)) + MWBase::Environment::get().getWindowManager()->messageBox("#{sKilledEssential}"); } - else - MWBase::Environment::get().getWindowManager()->setSneakVisibility(false); } } + void Actors::restoreDynamicStats(bool sleep) { for(PtrControllerMap::iterator iter(mActors.begin());iter != mActors.end();++iter) - calculateRestoration(iter->first, 3600, sleep); + restoreDynamicStats(iter->first, sleep); } int Actors::getHoursToRest(const MWWorld::Ptr &ptr) const @@ -1084,10 +1393,10 @@ namespace MWMechanics CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); - float healthHours = healthPerHour >= 0 + float healthHours = healthPerHour > 0 ? (stats.getHealth().getModified() - stats.getHealth().getCurrent()) / healthPerHour : 1.0f; - float magickaHours = magickaPerHour >= 0 + float magickaHours = magickaPerHour > 0 ? (stats.getMagicka().getModified() - stats.getMagicka().getCurrent()) / magickaPerHour : 1.0f; @@ -1143,15 +1452,28 @@ namespace MWMechanics std::list Actors::getActorsFollowing(const MWWorld::Ptr& actor) { std::list list; - for(PtrControllerMap::iterator iter(mActors.begin());iter != mActors.end();iter++) + for(PtrControllerMap::iterator iter(mActors.begin());iter != mActors.end();++iter) { const MWWorld::Class &cls = iter->first.getClass(); CreatureStats &stats = cls.getCreatureStats(iter->first); - if(!stats.isDead() && stats.getAiSequence().getTypeId() == AiPackage::TypeIdFollow) + if (stats.isDead()) + continue; + + // An actor counts as following if AiFollow is the current AiPackage, or there are only Combat packages before the AiFollow package + for (std::list::const_iterator it = stats.getAiSequence().begin(); it != stats.getAiSequence().end(); ++it) { - MWMechanics::AiFollow* package = static_cast(stats.getAiSequence().getActivePackage()); - if(package->getFollowedActor() == actor.getCellRef().getRefId()) - list.push_front(iter->first); + if ((*it)->getTypeId() == MWMechanics::AiPackage::TypeIdFollow) + { + MWWorld::Ptr followTarget = dynamic_cast(*it)->getTarget(); + if (followTarget.isEmpty()) + continue; + if (followTarget == actor) + list.push_back(iter->first); + else + break; + } + else if ((*it)->getTypeId() != MWMechanics::AiPackage::TypeIdCombat) + break; } } return list; @@ -1164,17 +1486,60 @@ namespace MWMechanics getObjectsInRange(position, MWBase::Environment::get().getWorld()->getStore().get().find("fAlarmRadius")->getFloat(), neighbors); //only care about those within the alarm disance - for(std::vector::iterator iter(neighbors.begin());iter != neighbors.end();iter++) + for(std::vector::iterator iter(neighbors.begin());iter != neighbors.end();++iter) { const MWWorld::Class &cls = iter->getClass(); CreatureStats &stats = cls.getCreatureStats(*iter); - if(!stats.isDead() && stats.getAiSequence().getTypeId() == AiPackage::TypeIdCombat) + if (!stats.isDead() && stats.getAiSequence().isInCombat(actor)) + list.push_front(*iter); + } + return list; + } + + void Actors::write (ESM::ESMWriter& writer, Loading::Listener& listener) const + { + writer.startRecord(ESM::REC_DCOU); + for (std::map::const_iterator it = mDeathCount.begin(); it != mDeathCount.end(); ++it) + { + writer.writeHNString("ID__", it->first); + writer.writeHNT ("COUN", it->second); + } + writer.endRecord(ESM::REC_DCOU); + + listener.increaseProgress(1); + } + + void Actors::readRecord (ESM::ESMReader& reader, int32_t type) + { + if (type == ESM::REC_DCOU) + { + while (reader.isNextSub("ID__")) { - MWMechanics::AiCombat* package = static_cast(stats.getAiSequence().getActivePackage()); - if(package->getTarget() == actor) - list.push_front(*iter); + std::string id = reader.getHString(); + int count; + reader.getHNT (count, "COUN"); + mDeathCount[id] = count; } } - return list; + } + + void Actors::clear() + { + PtrControllerMap::iterator it(mActors.begin()); + for (; it != mActors.end(); ++it) + { + delete it->second; + it->second = NULL; + } + mActors.clear(); + mDeathCount.clear(); + } + + void Actors::updateMagicEffects(const MWWorld::Ptr &ptr) + { + adjustMagicEffects(ptr); + calculateCreatureStatModifiers(ptr, 0.f); + if (ptr.getClass().isNpc()) + calculateNpcStatModifiers(ptr, 0.f); } } diff --git a/apps/openmw/mwmechanics/actors.hpp b/apps/openmw/mwmechanics/actors.hpp index cc95660dc..0ccfaad78 100644 --- a/apps/openmw/mwmechanics/actors.hpp +++ b/apps/openmw/mwmechanics/actors.hpp @@ -34,9 +34,9 @@ namespace MWMechanics void calculateDynamicStats (const MWWorld::Ptr& ptr); void calculateCreatureStatModifiers (const MWWorld::Ptr& ptr, float duration); - void calculateNpcStatModifiers (const MWWorld::Ptr& ptr); + void calculateNpcStatModifiers (const MWWorld::Ptr& ptr, float duration); - void calculateRestoration (const MWWorld::Ptr& ptr, float duration, bool sleep); + void calculateRestoration (const MWWorld::Ptr& ptr, float duration); void updateDrowning (const MWWorld::Ptr& ptr, float duration); @@ -44,6 +44,8 @@ namespace MWMechanics void updateCrimePersuit (const MWWorld::Ptr& ptr, float duration); + void killDeadActors (); + public: Actors(); @@ -56,7 +58,7 @@ namespace MWMechanics /// Update magic effects for an actor. Usually done automatically once per frame, but if we're currently /// paused we may want to do it manually (after equipping permanent enchantment) - void updateMagicEffects (const MWWorld::Ptr& ptr) { adjustMagicEffects(ptr); } + void updateMagicEffects (const MWWorld::Ptr& ptr); void addActor (const MWWorld::Ptr& ptr, bool updateImmediately=false); ///< Register an actor for stats management @@ -90,6 +92,8 @@ namespace MWMechanics void restoreDynamicStats(bool sleep); ///< If the player is sleeping, this should be called every hour. + void restoreDynamicStats(const MWWorld::Ptr& actor, bool sleep); + int getHoursToRest(const MWWorld::Ptr& ptr) const; ///< Calculate how many hours the given actor needs to rest in order to be fully healed @@ -112,6 +116,12 @@ namespace MWMechanics /**ie AiCombat is active and the target is the actor **/ std::list getActorsFighting(const MWWorld::Ptr& actor); + void write (ESM::ESMWriter& writer, Loading::Listener& listener) const; + + void readRecord (ESM::ESMReader& reader, int32_t type); + + void clear(); // Clear death counter + private: PtrControllerMap mActors; diff --git a/apps/openmw/mwmechanics/aiactivate.cpp b/apps/openmw/mwmechanics/aiactivate.cpp index 3dfacb853..9e25084d3 100644 --- a/apps/openmw/mwmechanics/aiactivate.cpp +++ b/apps/openmw/mwmechanics/aiactivate.cpp @@ -1,10 +1,13 @@ #include "aiactivate.hpp" +#include + #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" +#include "../mwmechanics/creaturestats.hpp" + #include "../mwworld/class.hpp" -#include "../mwworld/action.hpp" #include "../mwworld/cellstore.hpp" #include "steering.hpp" @@ -18,27 +21,34 @@ MWMechanics::AiActivate *MWMechanics::AiActivate::clone() const { return new AiActivate(*this); } -bool MWMechanics::AiActivate::execute (const MWWorld::Ptr& actor,float duration) -{ - ESM::Position pos = actor.getRefData().getPosition(); //position of the actor - const MWWorld::Ptr target = MWBase::Environment::get().getWorld()->searchPtr(mObjectId, false); //The target to follow - - if(target == MWWorld::Ptr()) - return true; //Target doesn't exist - - //Set the target desition from the actor - ESM::Pathgrid::Point dest = target.getRefData().getPosition().pos; - - if(distance(dest, pos.pos[0], pos.pos[1], pos.pos[2]) < 200) { //Stop when you get close - actor.getClass().getMovementSettings(actor).mPosition[1] = 0; - MWWorld::Ptr target = MWBase::Environment::get().getWorld()->getPtr(mObjectId,false); - target.getClass().activate(target,actor).get()->execute(actor); //Arrest player - return true; - } - else { - pathTo(actor, dest, duration); //Go to the destination - } - +bool MWMechanics::AiActivate::execute (const MWWorld::Ptr& actor, AiState& state, float duration) +{ + ESM::Position pos = actor.getRefData().getPosition(); //position of the actor + const MWWorld::Ptr target = MWBase::Environment::get().getWorld()->searchPtr(mObjectId, false); //The target to follow + + actor.getClass().getCreatureStats(actor).setDrawState(DrawState_Nothing); + + if(target == MWWorld::Ptr() || + !target.getRefData().getCount() || !target.getRefData().isEnabled() // Really we should be checking whether the target is currently registered + // with the MechanicsManager + ) + return true; //Target doesn't exist + + //Set the target desition from the actor + ESM::Pathgrid::Point dest = target.getRefData().getPosition().pos; + + if(distance(dest, pos.pos[0], pos.pos[1], pos.pos[2]) < 200) { //Stop when you get close + actor.getClass().getMovementSettings(actor).mPosition[1] = 0; + MWWorld::Ptr target = MWBase::Environment::get().getWorld()->getPtr(mObjectId,false); + + MWBase::Environment::get().getWorld()->activate(target, actor); + + return true; + } + else { + pathTo(actor, dest, duration); //Go to the destination + } + return false; } @@ -46,3 +56,20 @@ int MWMechanics::AiActivate::getTypeId() const { return TypeIdActivate; } + +void MWMechanics::AiActivate::writeState(ESM::AiSequence::AiSequence &sequence) const +{ + std::auto_ptr activate(new ESM::AiSequence::AiActivate()); + activate->mTargetId = mObjectId; + + ESM::AiSequence::AiPackageContainer package; + package.mType = ESM::AiSequence::Ai_Activate; + package.mPackage = activate.release(); + sequence.mPackages.push_back(package); +} + +MWMechanics::AiActivate::AiActivate(const ESM::AiSequence::AiActivate *activate) + : mObjectId(activate->mTargetId) +{ + +} diff --git a/apps/openmw/mwmechanics/aiactivate.hpp b/apps/openmw/mwmechanics/aiactivate.hpp index 0e660e967..e25afe285 100644 --- a/apps/openmw/mwmechanics/aiactivate.hpp +++ b/apps/openmw/mwmechanics/aiactivate.hpp @@ -6,24 +6,35 @@ #include "pathfinding.hpp" +namespace ESM +{ +namespace AiSequence +{ + struct AiActivate; +} +} + namespace MWMechanics -{ - /// \brief Causes actor to walk to activatable object and activate it +{ + /// \brief Causes actor to walk to activatable object and activate it /** Will activate when close to object **/ class AiActivate : public AiPackage { - public: - /// Constructor + public: + /// Constructor /** \param objectId Reference to object to activate **/ AiActivate(const std::string &objectId); + + AiActivate(const ESM::AiSequence::AiActivate* activate); + virtual AiActivate *clone() const; - virtual bool execute (const MWWorld::Ptr& actor,float duration); + virtual bool execute (const MWWorld::Ptr& actor, AiState& state, float duration); virtual int getTypeId() const; + virtual void writeState(ESM::AiSequence::AiSequence& sequence) const; + private: std::string mObjectId; - int mCellX; - int mCellY; }; } #endif // GAME_MWMECHANICS_AIACTIVATE_H diff --git a/apps/openmw/mwmechanics/aiavoiddoor.cpp b/apps/openmw/mwmechanics/aiavoiddoor.cpp index ea6f296cc..b9954337d 100644 --- a/apps/openmw/mwmechanics/aiavoiddoor.cpp +++ b/apps/openmw/mwmechanics/aiavoiddoor.cpp @@ -18,7 +18,7 @@ MWMechanics::AiAvoidDoor::AiAvoidDoor(const MWWorld::Ptr& doorPtr) } -bool MWMechanics::AiAvoidDoor::execute (const MWWorld::Ptr& actor,float duration) +bool MWMechanics::AiAvoidDoor::execute (const MWWorld::Ptr& actor, AiState& state, float duration) { ESM::Position pos = actor.getRefData().getPosition(); @@ -51,17 +51,20 @@ bool MWMechanics::AiAvoidDoor::execute (const MWWorld::Ptr& actor,float duration ESM::Position tPos = mDoorPtr.getRefData().getPosition(); //Position of the door float x = pos.pos[0] - tPos.pos[0]; float y = pos.pos[1] - tPos.pos[1]; - float dirToDoor = std::atan2(x,y) + pos.rot[2] + mAdjAngle; //Calculates the direction to the door, relative to the direction of the NPC - // For example, if the NPC is directly facing the door this will be pi/2 - // Make actor move away from the door - actor.getClass().getMovementSettings(actor).mPosition[1] = -1 * std::sin(dirToDoor); //I knew I'd use trig someday - actor.getClass().getMovementSettings(actor).mPosition[0] = -1 * std::cos(dirToDoor); + actor.getClass().getCreatureStats(actor).setMovementFlag(CreatureStats::Flag_Run, true); - //Make all nearby actors also avoid the door + // Turn away from the door and move when turn completed + if (zTurn(actor, Ogre::Radian(std::atan2(x,y) + mAdjAngle), Ogre::Degree(5))) + actor.getClass().getMovementSettings(actor).mPosition[1] = 1; + else + actor.getClass().getMovementSettings(actor).mPosition[1] = 0; + actor.getClass().getMovementSettings(actor).mPosition[0] = 0; + + // Make all nearby actors also avoid the door std::vector actors; MWBase::Environment::get().getMechanicsManager()->getActorsInRange(Ogre::Vector3(pos.pos[0],pos.pos[1],pos.pos[2]),100,actors); - for(std::vector::iterator it = actors.begin(); it != actors.end(); it++) { + for(std::vector::iterator it = actors.begin(); it != actors.end(); ++it) { if(*it != MWBase::Environment::get().getWorld()->getPlayerPtr()) { //Not the player MWMechanics::AiSequence& seq = it->getClass().getCreatureStats(*it).getAiSequence(); if(seq.getTypeId() != MWMechanics::AiPackage::TypeIdAvoidDoor) { //Only add it once @@ -78,8 +81,14 @@ MWMechanics::AiAvoidDoor *MWMechanics::AiAvoidDoor::clone() const return new AiAvoidDoor(*this); } - int MWMechanics::AiAvoidDoor::getTypeId() const +int MWMechanics::AiAvoidDoor::getTypeId() const { return TypeIdAvoidDoor; } +unsigned int MWMechanics::AiAvoidDoor::getPriority() const +{ + return 2; +} + + diff --git a/apps/openmw/mwmechanics/aiavoiddoor.hpp b/apps/openmw/mwmechanics/aiavoiddoor.hpp index d2a2e33a1..7590c8fcb 100644 --- a/apps/openmw/mwmechanics/aiavoiddoor.hpp +++ b/apps/openmw/mwmechanics/aiavoiddoor.hpp @@ -20,10 +20,12 @@ namespace MWMechanics virtual AiAvoidDoor *clone() const; - virtual bool execute (const MWWorld::Ptr& actor,float duration); + virtual bool execute (const MWWorld::Ptr& actor, AiState& state, float duration); virtual int getTypeId() const; + virtual unsigned int getPriority() const; + private: float mDuration; MWWorld::Ptr mDoorPtr; diff --git a/apps/openmw/mwmechanics/aicombat.cpp b/apps/openmw/mwmechanics/aicombat.cpp index 9ec770c9d..67fd54456 100644 --- a/apps/openmw/mwmechanics/aicombat.cpp +++ b/apps/openmw/mwmechanics/aicombat.cpp @@ -1,8 +1,8 @@ #include "aicombat.hpp" #include -#include +#include #include "../mwworld/class.hpp" #include "../mwworld/timestamp.hpp" @@ -14,34 +14,36 @@ #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/dialoguemanager.hpp" +#include "../mwrender/animation.hpp" + #include "creaturestats.hpp" #include "steering.hpp" #include "movement.hpp" #include "character.hpp" // fixme: for getActiveWeapon +#include "aicombataction.hpp" + namespace { - static float sgn(Ogre::Radian a) - { - if(a.valueDegrees() > 0) - return 1.0; - return -1.0; - } //chooses an attack depending on probability to avoid uniformity - void chooseBestAttack(const ESM::Weapon* weapon, MWMechanics::Movement &movement); + ESM::Weapon::AttackType chooseBestAttack(const ESM::Weapon* weapon, MWMechanics::Movement &movement); + + void getMinMaxAttackDuration(const MWWorld::Ptr& actor, float (*fMinMaxDurations)[2]); + + Ogre::Vector3 AimDirToMovingTarget(const MWWorld::Ptr& actor, const MWWorld::Ptr& target, const Ogre::Vector3& vLastTargetPos, + float duration, int weapType, float strength); - float getZAngleToDir(const Ogre::Vector3& dir, float dirLen = 0.0f) + float getZAngleToDir(const Ogre::Vector3& dir) { - float len = (dirLen > 0.0f)? dirLen : dir.length(); - return Ogre::Radian( Ogre::Math::ACos(dir.y / len) * sgn(Ogre::Math::ASin(dir.x / len)) ).valueDegrees(); + return Ogre::Math::ATan2(dir.x,dir.y).valueDegrees(); } float getXAngleToDir(const Ogre::Vector3& dir, float dirLen = 0.0f) { float len = (dirLen > 0.0f)? dirLen : dir.length(); - return Ogre::Radian(-Ogre::Math::ASin(dir.z / len)).valueDegrees(); + return -Ogre::Math::ASin(dir.z / len).valueDegrees(); } @@ -76,28 +78,63 @@ namespace namespace MWMechanics { - static const float MAX_ATTACK_DURATION = 0.35f; static const float DOOR_CHECK_INTERVAL = 1.5f; // same as AiWander // NOTE: MIN_DIST_TO_DOOR_SQUARED is defined in obstacle.hpp - AiCombat::AiCombat(const MWWorld::Ptr& actor) : - mTargetActorId(actor.getClass().getCreatureStats(actor).getActorId()), + + /// \brief This class holds the variables AiCombat needs which are deleted if the package becomes inactive. + struct AiCombatStorage : AiTemporaryBase + { + float mTimerAttack; + float mTimerReact; + float mTimerCombatMove; + bool mReadyToAttack; + bool mAttack; + bool mFollowTarget; + bool mCombatMove; + Ogre::Vector3 mLastTargetPos; + const MWWorld::CellStore* mCell; + boost::shared_ptr mCurrentAction; + float mActionCooldown; + float mStrength; + float mMinMaxAttackDuration[3][2]; + bool mMinMaxAttackDurationInitialised; + bool mForceNoShortcut; + ESM::Position mShortcutFailPos; + Ogre::Vector3 mLastActorPos; + MWMechanics::Movement mMovement; + + AiCombatStorage(): mTimerAttack(0), mTimerReact(0), mTimerCombatMove(0), - mFollowTarget(false), - mReadyToAttack(false), mAttack(false), + mFollowTarget(false), mCombatMove(false), - mMovement(), + mReadyToAttack(false), mForceNoShortcut(false), - mShortcutFailPos(), - mBackOffDoor(false), mCell(NULL), - mDoorIter(actor.getCell()->get().mList.end()), - mDoors(actor.getCell()->get()), - mDoorCheckDuration(0) + mCurrentAction(), + mActionCooldown(0), + mStrength(), + mMinMaxAttackDurationInitialised(false), + mLastTargetPos(0,0,0), + mLastActorPos(0,0,0), + mMovement(){} + }; + + AiCombat::AiCombat(const MWWorld::Ptr& actor) : + mTargetActorId(actor.getClass().getCreatureStats(actor).getActorId()) + {} + + AiCombat::AiCombat(const ESM::AiSequence::AiCombat *combat) + { + mTargetActorId = combat->mTargetActorId; + } + + void AiCombat::init() { + } /* @@ -146,150 +183,238 @@ namespace MWMechanics * Use the Observer Pattern to co-ordinate attacks, provide intelligence on * whether the target was hit, etc. */ - bool AiCombat::execute (const MWWorld::Ptr& actor,float duration) + bool AiCombat::execute (const MWWorld::Ptr& actor, AiState& state, float duration) { + // get or create temporary storage + AiCombatStorage& storage = state.get(); + + + //General description if(actor.getClass().getCreatureStats(actor).isDead()) return true; MWWorld::Ptr target = MWBase::Environment::get().getWorld()->searchPtrViaActorId(mTargetActorId); + if (target.isEmpty()) + return false; - if(target.getClass().getCreatureStats(target).isDead()) + if(!target.getRefData().getCount() || !target.getRefData().isEnabled() // Really we should be checking whether the target is currently registered + // with the MechanicsManager + || target.getClass().getCreatureStats(target).isDead()) return true; const MWWorld::Class& actorClass = actor.getClass(); - MWBase::World& world = *MWBase::Environment::get().getWorld(); - - if ((!actorClass.isNpc() && target == world.getPlayerPtr() && - actorClass.canSwim(actor) && !actorClass.canWalk(actor) // pure water creature - && !world.isSwimming(target)) // Player moved out of water - || (!actorClass.canSwim(actor) && world.isSwimming(target))) // creature can't swim to Player + MWBase::World* world = MWBase::Environment::get().getWorld(); + + if (!actorClass.isNpc() && + // 1. pure water creature and Player moved out of water + ((target == world->getPlayerPtr() && + actorClass.canSwim(actor) && !actor.getClass().canWalk(actor) && !world->isSwimming(target)) + // 2. creature can't swim to target + || (!actorClass.canSwim(actor) && world->isSwimming(target)))) { - actorClass.getCreatureStats(actor).setHostile(false); actorClass.getCreatureStats(actor).setAttackingOrSpell(false); return true; } + + + + //Update every frame - if(mCombatMove) + bool& combatMove = storage.mCombatMove; + float& timerCombatMove = storage.mTimerCombatMove; + MWMechanics::Movement& movement = storage.mMovement; + if(combatMove) { - mTimerCombatMove -= duration; - if( mTimerCombatMove <= 0) + timerCombatMove -= duration; + if( timerCombatMove <= 0) { - mTimerCombatMove = 0; - mMovement.mPosition[1] = mMovement.mPosition[0] = 0; - mCombatMove = false; + timerCombatMove = 0; + movement.mPosition[1] = movement.mPosition[0] = 0; + combatMove = false; } } - actor.getClass().getMovementSettings(actor) = mMovement; - actor.getClass().getMovementSettings(actor).mRotation[0] = 0; - actor.getClass().getMovementSettings(actor).mRotation[2] = 0; + actorClass.getMovementSettings(actor) = movement; + actorClass.getMovementSettings(actor).mRotation[0] = 0; + actorClass.getMovementSettings(actor).mRotation[2] = 0; + + if(movement.mRotation[2] != 0) + { + if(zTurn(actor, Ogre::Degree(movement.mRotation[2]))) movement.mRotation[2] = 0; + } - if(mMovement.mRotation[2] != 0) + if(movement.mRotation[0] != 0) { - if(zTurn(actor, Ogre::Degree(mMovement.mRotation[2]))) mMovement.mRotation[2] = 0; + if(smoothTurn(actor, Ogre::Degree(movement.mRotation[0]), 0)) movement.mRotation[0] = 0; } - if(mMovement.mRotation[0] != 0) + //TODO: Some skills affect period of strikes.For berserk-like style period ~ 0.25f + float attacksPeriod = 1.0f; + + ESM::Weapon::AttackType attackType; + + + + + bool& attack = storage.mAttack; + bool& readyToAttack = storage.mReadyToAttack; + float& timerAttack = storage.mTimerAttack; + + bool& minMaxAttackDurationInitialised = storage.mMinMaxAttackDurationInitialised; + float (&minMaxAttackDuration)[3][2] = storage.mMinMaxAttackDuration; + + if(readyToAttack) + { + if (!minMaxAttackDurationInitialised) + { + // TODO: this must be updated when a different weapon is equipped + getMinMaxAttackDuration(actor, minMaxAttackDuration); + minMaxAttackDurationInitialised = true; + } + + if (timerAttack < 0) attack = false; + + timerAttack -= duration; + } + else { - if(smoothTurn(actor, Ogre::Degree(mMovement.mRotation[0]), 0)) mMovement.mRotation[0] = 0; + timerAttack = -attacksPeriod; + attack = false; } - mTimerAttack -= duration; - actor.getClass().getCreatureStats(actor).setAttackingOrSpell(mAttack); + actorClass.getCreatureStats(actor).setAttackingOrSpell(attack); + + float& actionCooldown = storage.mActionCooldown; + actionCooldown -= duration; + + float& timerReact = storage.mTimerReact; float tReaction = 0.25f; - if(mTimerReact < tReaction) + if(timerReact < tReaction) { - mTimerReact += duration; + timerReact += duration; return false; } //Update with period = tReaction - mTimerReact = 0; - - bool cellChange = mCell && (actor.getCell() != mCell); - if(!mCell || cellChange) + timerReact = 0; + const MWWorld::CellStore*& currentCell = storage.mCell; + bool cellChange = currentCell && (actor.getCell() != currentCell); + if(!currentCell || cellChange) { - mCell = actor.getCell(); + currentCell = actor.getCell(); } - //actual attacking logic - //TODO: Some skills affect period of strikes.For berserk-like style period ~ 0.25f - float attacksPeriod = 1.0f; - if(mReadyToAttack) - { - if(mTimerAttack <= -attacksPeriod) - { - //TODO: should depend on time between 'start' to 'min attack' - //for better controlling of NPCs' attack strength. - //Also it seems that this time is different for slash/thrust/chop - mTimerAttack = MAX_ATTACK_DURATION * static_cast(rand())/RAND_MAX; - mAttack = true; + MWRender::Animation* anim = MWBase::Environment::get().getWorld()->getAnimation(actor); + if (!anim) // shouldn't happen + return false; - //say a provoking combat phrase - if (actor.getClass().isNpc()) - { - const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); - int chance = store.get().find("iVoiceAttackOdds")->getInt(); - int roll = std::rand()/ (static_cast (RAND_MAX) + 1) * 100; // [0, 99] - if (roll < chance) - { - MWBase::Environment::get().getDialogueManager()->say(actor, "attack"); - } - } - } - else if (mTimerAttack <= 0) - mAttack = false; - } - else + actorClass.getCreatureStats(actor).setMovementFlag(CreatureStats::Flag_Run, true); + + if (actionCooldown > 0) + return false; + + float rangeAttack = 0; + float rangeFollow = 0; + boost::shared_ptr& currentAction = storage.mCurrentAction; + if (anim->upperBodyReady()) { - mTimerAttack = -attacksPeriod; - mAttack = false; + currentAction = prepareNextAction(actor, target); + actionCooldown = currentAction->getActionCooldown(); } + if (currentAction.get()) + currentAction->getCombatRange(rangeAttack, rangeFollow); - const MWWorld::Class &actorCls = actor.getClass(); + // FIXME: consider moving this stuff to ActionWeapon::getCombatRange const ESM::Weapon *weapon = NULL; - MWMechanics::WeaponType weaptype; - float weapRange, weapSpeed = 1.0f; - - actorCls.getCreatureStats(actor).setMovementFlag(CreatureStats::Flag_Run, true); + MWMechanics::WeaponType weaptype = WeapType_None; + float weapRange = 1.0f; // Get weapon characteristics - if (actorCls.hasInventoryStore(actor)) + if (actorClass.hasInventoryStore(actor)) { - MWMechanics::DrawState_ state = actorCls.getCreatureStats(actor).getDrawState(); - if (state == MWMechanics::DrawState_Spell || state == MWMechanics::DrawState_Nothing) - actorCls.getCreatureStats(actor).setDrawState(MWMechanics::DrawState_Weapon); - // TODO: Check equipped weapon and equip a different one if we can't attack with it // (e.g. no ammunition, or wrong type of ammunition equipped, etc. autoEquip is not very smart in this regard)) //Get weapon speed and range MWWorld::ContainerStoreIterator weaponSlot = - MWMechanics::getActiveWeapon(actorCls.getCreatureStats(actor), actorCls.getInventoryStore(actor), &weaptype); + MWMechanics::getActiveWeapon(actorClass.getCreatureStats(actor), actorClass.getInventoryStore(actor), &weaptype); if (weaptype == WeapType_HandToHand) { - const MWWorld::Store &gmst = - MWBase::Environment::get().getWorld()->getStore().get(); - weapRange = gmst.find("fHandToHandReach")->getFloat(); + static float fHandToHandReach = + world->getStore().get().find("fHandToHandReach")->getFloat(); + weapRange = fHandToHandReach; } - else if (weaptype != WeapType_PickProbe && weaptype != WeapType_Spell) + else if (weaptype != WeapType_PickProbe && weaptype != WeapType_Spell && weaptype != WeapType_None) { // All other WeapTypes are actually weapons, so get is safe. weapon = weaponSlot->get()->mBase; weapRange = weapon->mData.mReach; - weapSpeed = weapon->mData.mSpeed; } weapRange *= 100.0f; } else //is creature { - weaptype = WeapType_HandToHand; //doesn't matter, should only reflect if it is melee or distant weapon - weapRange = 150; //TODO: use true attack range (the same problem in Creature::hit) + weaptype = actorClass.getCreatureStats(actor).getDrawState() == DrawState_Spell ? WeapType_Spell : WeapType_HandToHand; + weapRange = 150.0f; //TODO: use true attack range (the same problem in Creature::hit) + } + + bool distantCombat = false; + if (weaptype != WeapType_Spell) + { + // TODO: move to ActionWeapon + if (weaptype == WeapType_BowAndArrow || weaptype == WeapType_Crossbow || weaptype == WeapType_Thrown) + { + rangeAttack = 1000; + rangeFollow = 0; // not needed in ranged combat + distantCombat = true; + } + else + { + rangeAttack = weapRange; + rangeFollow = 300; + } + } + else + { + distantCombat = (rangeAttack > 500); + weapRange = 150.f; + } + + + float& strength = storage.mStrength; + // start new attack + if(readyToAttack) + { + if(timerAttack <= -attacksPeriod) + { + attack = true; // attack starts just now + + if (!distantCombat) attackType = chooseBestAttack(weapon, movement); + else attackType = ESM::Weapon::AT_Chop; // cause it's =0 + + strength = static_cast(rand()) / RAND_MAX; + + // Note: may be 0 for some animations + timerAttack = minMaxAttackDuration[attackType][0] + + (minMaxAttackDuration[attackType][1] - minMaxAttackDuration[attackType][0]) * strength; + + //say a provoking combat phrase + if (actor.getClass().isNpc()) + { + const MWWorld::ESMStore &store = world->getStore(); + int chance = store.get().find("iVoiceAttackOdds")->getInt(); + int roll = std::rand()/ (static_cast (RAND_MAX) + 1) * 100; // [0, 99] + if (roll < chance) + { + MWBase::Environment::get().getDialogueManager()->say(actor, "attack"); + } + } + } } @@ -322,101 +447,100 @@ namespace MWMechanics * target even if LOS is not achieved) */ - float rangeAttack; - float rangeFollow; - bool distantCombat = false; - if (weaptype == WeapType_BowAndArrow || weaptype == WeapType_Crossbow || weaptype == WeapType_Thrown) - { - rangeAttack = 1000; // TODO: should depend on archer skill - rangeFollow = 0; // not needed in ranged combat - distantCombat = true; - } - else - { - rangeAttack = weapRange; - rangeFollow = 300; - } - ESM::Position pos = actor.getRefData().getPosition(); Ogre::Vector3 vActorPos(pos.pos); Ogre::Vector3 vTargetPos(target.getRefData().getPosition().pos); Ogre::Vector3 vDirToTarget = vTargetPos - vActorPos; float distToTarget = vDirToTarget.length(); + + Ogre::Vector3& lastActorPos = storage.mLastActorPos; + bool& followTarget = storage.mFollowTarget; bool isStuck = false; float speed = 0.0f; - if(mMovement.mPosition[1] && (Ogre::Vector3(mLastPos.pos) - vActorPos).length() < (speed = actorCls.getSpeed(actor)) * tReaction / 2) + if(movement.mPosition[1] && (lastActorPos - vActorPos).length() < (speed = actorClass.getSpeed(actor)) * tReaction / 2) isStuck = true; - mLastPos = pos; + lastActorPos = vActorPos; // check if actor can move along z-axis - bool canMoveByZ = (actorCls.canSwim(actor) && MWBase::Environment::get().getWorld()->isSwimming(actor)) - || MWBase::Environment::get().getWorld()->isFlying(actor); + bool canMoveByZ = (actorClass.canSwim(actor) && world->isSwimming(actor)) + || world->isFlying(actor); - // determine vertical angle to target - // if actor can move along z-axis it will control movement dir - // if can't - it will control correct aiming - mMovement.mRotation[0] = getXAngleToDir(vDirToTarget, distToTarget); + // for distant combat we should know if target is in LOS even if distToTarget < rangeAttack + bool inLOS = distantCombat ? world->getLOS(actor, target) : true; - // (within strike dist) || (not quite strike dist while following) - if(distToTarget < rangeAttack || (distToTarget <= rangeFollow && mFollowTarget && !isStuck) ) + // (within attack dist) || (not quite attack dist while following) + if(inLOS && (distToTarget < rangeAttack || (distToTarget <= rangeFollow && followTarget && !isStuck))) { //Melee and Close-up combat + + // getXAngleToDir determines vertical angle to target: + // if actor can move along z-axis it will control movement dir + // if can't - it will control correct aiming. + // note: in getZAngleToDir if we preserve dir.z then horizontal angle can be inaccurate + if (distantCombat) + { + Ogre::Vector3& lastTargetPos = storage.mLastTargetPos; + Ogre::Vector3 vAimDir = AimDirToMovingTarget(actor, target, lastTargetPos, tReaction, weaptype, strength); + lastTargetPos = vTargetPos; + movement.mRotation[0] = getXAngleToDir(vAimDir); + movement.mRotation[2] = getZAngleToDir(vAimDir); + } + else + { + movement.mRotation[0] = getXAngleToDir(vDirToTarget, distToTarget); + movement.mRotation[2] = getZAngleToDir(vDirToTarget); + } - // if we preserve dir.z then horizontal angle can be inaccurate - mMovement.mRotation[2] = getZAngleToDir(Ogre::Vector3(vDirToTarget.x, vDirToTarget.y, 0)); - - // (not quite strike dist while following) - if (mFollowTarget && distToTarget > rangeAttack) + // (not quite attack dist while following) + if (followTarget && distToTarget > rangeAttack) { //Close-up combat: just run up on target - mMovement.mPosition[1] = 1; + movement.mPosition[1] = 1; } - else // (within strike dist) + else // (within attack dist) { - mMovement.mPosition[1] = 0; - - // set slash/thrust/chop attack - if (mAttack && !distantCombat) chooseBestAttack(weapon, mMovement); - - if(mMovement.mPosition[0] || mMovement.mPosition[1]) + if(movement.mPosition[0] || movement.mPosition[1]) { - mTimerCombatMove = 0.1f + 0.1f * static_cast(rand())/RAND_MAX; - mCombatMove = true; + timerCombatMove = 0.1f + 0.1f * static_cast(rand())/RAND_MAX; + combatMove = true; } // only NPCs are smart enough to use dodge movements - else if(actorCls.isNpc() && (!distantCombat || (distantCombat && distToTarget < rangeAttack/2))) + else if(actorClass.isNpc() && (!distantCombat || (distantCombat && distToTarget < rangeAttack/2))) { //apply sideway movement (kind of dodging) with some probability if(static_cast(rand())/RAND_MAX < 0.25) { - mMovement.mPosition[0] = static_cast(rand())/RAND_MAX < 0.5? 1: -1; - mTimerCombatMove = 0.05f + 0.15f * static_cast(rand())/RAND_MAX; - mCombatMove = true; + movement.mPosition[0] = static_cast(rand())/RAND_MAX < 0.5? 1: -1; + timerCombatMove = 0.05f + 0.15f * static_cast(rand())/RAND_MAX; + combatMove = true; } } if(distantCombat && distToTarget < rangeAttack/4) { - mMovement.mPosition[1] = -1; + movement.mPosition[1] = -1; } - mReadyToAttack = true; + readyToAttack = true; //only once got in melee combat, actor is allowed to use close-up shortcutting - mFollowTarget = true; + followTarget = true; } } else // remote pathfinding { bool preferShortcut = false; - bool inLOS = MWBase::Environment::get().getWorld()->getLOS(actor, target); + if (!distantCombat) inLOS = world->getLOS(actor, target); // check if shortcut is available - if(inLOS && (!isStuck || mReadyToAttack) - && (!mForceNoShortcut || (Ogre::Vector3(mShortcutFailPos.pos) - vActorPos).length() >= PATHFIND_SHORTCUT_RETRY_DIST)) + bool& forceNoShortcut = storage.mForceNoShortcut; + ESM::Position& shortcutFailPos = storage.mShortcutFailPos; + + if(inLOS && (!isStuck || readyToAttack) + && (!forceNoShortcut || (Ogre::Vector3(shortcutFailPos.pos) - vActorPos).length() >= PATHFIND_SHORTCUT_RETRY_DIST)) { - if(speed == 0.0f) speed = actorCls.getSpeed(actor); + if(speed == 0.0f) speed = actorClass.getSpeed(actor); // maximum dist before pit/obstacle for actor to avoid them depending on his speed float maxAvoidDist = tReaction * speed + speed / MAX_VEL_ANGULAR.valueRadians() * 2; // *2 - for reliability preferShortcut = checkWayIsClear(vActorPos, vTargetPos, Ogre::Vector3(vDirToTarget.x, vDirToTarget.y, 0).length() > maxAvoidDist*1.5? maxAvoidDist : maxAvoidDist/2); @@ -427,20 +551,22 @@ namespace MWMechanics if(preferShortcut) { - mMovement.mRotation[2] = getZAngleToDir(vDirToTarget, distToTarget); - mForceNoShortcut = false; - mShortcutFailPos.pos[0] = mShortcutFailPos.pos[1] = mShortcutFailPos.pos[2] = 0; + if (canMoveByZ) + movement.mRotation[0] = getXAngleToDir(vDirToTarget, distToTarget); + movement.mRotation[2] = getZAngleToDir(vDirToTarget); + forceNoShortcut = false; + shortcutFailPos.pos[0] = shortcutFailPos.pos[1] = shortcutFailPos.pos[2] = 0; mPathFinder.clearPath(); } else // if shortcut failed stick to path grid { - if(!isStuck && mShortcutFailPos.pos[0] == 0.0f && mShortcutFailPos.pos[1] == 0.0f && mShortcutFailPos.pos[2] == 0.0f) + if(!isStuck && shortcutFailPos.pos[0] == 0.0f && shortcutFailPos.pos[1] == 0.0f && shortcutFailPos.pos[2] == 0.0f) { - mForceNoShortcut = true; - mShortcutFailPos = pos; + forceNoShortcut = true; + shortcutFailPos = pos; } - mFollowTarget = false; + followTarget = false; buildNewPath(actor, target); //may fail to build a path, check before use @@ -458,7 +584,7 @@ namespace MWMechanics // if current actor pos is closer to target then last point of path (excluding target itself) then go straight on target if(distToTarget <= (vTargetPos - vBeforeTarget).length()) { - mMovement.mRotation[2] = getZAngleToDir(vDirToTarget, distToTarget); + movement.mRotation[2] = getZAngleToDir(vDirToTarget); preferShortcut = true; } } @@ -467,25 +593,31 @@ namespace MWMechanics if(!preferShortcut) { if(!mPathFinder.getPath().empty()) - mMovement.mRotation[2] = mPathFinder.getZAngleToNext(pos.pos[0], pos.pos[1]); + movement.mRotation[2] = mPathFinder.getZAngleToNext(pos.pos[0], pos.pos[1]); else - mMovement.mRotation[2] = getZAngleToDir(vDirToTarget, distToTarget); + movement.mRotation[2] = getZAngleToDir(vDirToTarget); } } - mMovement.mPosition[1] = 1; - mReadyToAttack = false; + movement.mPosition[1] = 1; + if (readyToAttack) + { + // to stop possible sideway moving after target moved out of attack range + combatMove = true; + timerCombatMove = 0; + } + readyToAttack = false; } if(!isStuck && distToTarget > rangeAttack && !distantCombat) { //special run attack; it shouldn't affect melee combat tactics - if(actorCls.getMovementSettings(actor).mPosition[1] == 1) + if(actorClass.getMovementSettings(actor).mPosition[1] == 1) { - //check if actor can overcome the distance = distToTarget - attackerWeapRange - //less than in time of playing weapon anim from 'start' to 'hit' tags (t_swing) - //then start attacking - float speed1 = actorCls.getSpeed(actor); + /* check if actor can overcome the distance = distToTarget - attackerWeapRange + less than in time of swinging with weapon (t_swing), then start attacking + */ + float speed1 = actorClass.getSpeed(actor); float speed2 = target.getClass().getSpeed(target); if(target.getClass().getMovementSettings(target).mPosition[0] == 0 && target.getClass().getMovementSettings(target).mPosition[1] == 0) @@ -494,14 +626,17 @@ namespace MWMechanics float s1 = distToTarget - weapRange; float t = s1/speed1; float s2 = speed2 * t; - float t_swing = (MAX_ATTACK_DURATION/2) / weapSpeed;//instead of 0.17 should be the time of playing weapon anim from 'start' to 'hit' tags + float t_swing = + minMaxAttackDuration[ESM::Weapon::AT_Thrust][0] + + (minMaxAttackDuration[ESM::Weapon::AT_Thrust][1] - minMaxAttackDuration[ESM::Weapon::AT_Thrust][0]) * static_cast(rand()) / RAND_MAX; + if (t + s2/speed1 <= t_swing) { - mReadyToAttack = true; - if(mTimerAttack <= -attacksPeriod) + readyToAttack = true; + if(timerAttack <= -attacksPeriod) { - mTimerAttack = MAX_ATTACK_DURATION * static_cast(rand())/RAND_MAX; - mAttack = true; + timerAttack = t_swing; + attack = true; } } } @@ -511,70 +646,22 @@ namespace MWMechanics // coded at 250ms or 1/4 second // // TODO: Add a parameter to vary DURATION_SAME_SPOT? - MWWorld::CellStore *cell = actor.getCell(); - if((distToTarget > rangeAttack || mFollowTarget) && + if((distToTarget > rangeAttack || followTarget) && mObstacleCheck.check(actor, tReaction)) // check if evasive action needed { - // first check if we're walking into a door - mDoorCheckDuration += 1.0f; // add time taken for obstacle check - if(mDoorCheckDuration >= DOOR_CHECK_INTERVAL && !cell->getCell()->isExterior()) - { - mDoorCheckDuration = 0; - // Check all the doors in this cell - mDoors = cell->get(); // update - mDoorIter = mDoors.mList.begin(); - for (; mDoorIter != mDoors.mList.end(); ++mDoorIter) - { - MWWorld::LiveCellRef& ref = *mDoorIter; - float minSqr = 1.3*1.3*MIN_DIST_TO_DOOR_SQUARED; // for legibility - if(vActorPos.squaredDistance(Ogre::Vector3(ref.mRef.getPosition().pos)) < minSqr && - ref.mData.getLocalRotation().rot[2] < 0.4f) // even small opening - { - //std::cout<<"closed door id \""<getCell()->isExterior() && !mDoors.mList.empty()) - { - MWWorld::LiveCellRef& ref = *mDoorIter; - float minSqr = 1.6 * 1.6 * MIN_DIST_TO_DOOR_SQUARED; // for legibility - // TODO: add reaction to checking open doors - if(mBackOffDoor && - vActorPos.squaredDistance(Ogre::Vector3(ref.mRef.getPosition().pos)) < minSqr) - { - mMovement.mPosition[1] = -0.2; // back off, but slowly - } - else if(mBackOffDoor && - mDoorIter != mDoors.mList.end() && - ref.mData.getLocalRotation().rot[2] >= 1) - { - mDoorIter = mDoors.mList.end(); - mBackOffDoor = false; - //std::cout<<"open door id \""< combat(new ESM::AiSequence::AiCombat()); + combat->mTargetActorId = mTargetActorId; + + ESM::AiSequence::AiPackageContainer package; + package.mType = ESM::AiSequence::Ai_Combat; + package.mPackage = combat.release(); + sequence.mPackages.push_back(package); + } } namespace { -void chooseBestAttack(const ESM::Weapon* weapon, MWMechanics::Movement &movement) +ESM::Weapon::AttackType chooseBestAttack(const ESM::Weapon* weapon, MWMechanics::Movement &movement) { + ESM::Weapon::AttackType attackType; + if (weapon == NULL) { //hand-to-hand deal equal damage for each type @@ -663,34 +763,169 @@ void chooseBestAttack(const ESM::Weapon* weapon, MWMechanics::Movement &movement { movement.mPosition[0] = (static_cast(rand())/RAND_MAX < 0.5f)? 1: -1; movement.mPosition[1] = 0; + attackType = ESM::Weapon::AT_Slash; } else if(roll <= 0.666f) //forward punch + { + movement.mPosition[1] = 1; + attackType = ESM::Weapon::AT_Thrust; + } + else + { + movement.mPosition[1] = movement.mPosition[0] = 0; + attackType = ESM::Weapon::AT_Chop; + } + } + else + { + //the more damage attackType deals the more probability it has + int slash = (weapon->mData.mSlash[0] + weapon->mData.mSlash[1])/2; + int chop = (weapon->mData.mChop[0] + weapon->mData.mChop[1])/2; + int thrust = (weapon->mData.mThrust[0] + weapon->mData.mThrust[1])/2; + + float total = slash + chop + thrust; + + float roll = static_cast(rand())/RAND_MAX; + if(roll <= static_cast(slash)/total) + { + movement.mPosition[0] = (static_cast(rand())/RAND_MAX < 0.5f)? 1: -1; + movement.mPosition[1] = 0; + attackType = ESM::Weapon::AT_Slash; + } + else if(roll <= (static_cast(slash) + static_cast(thrust))/total) + { movement.mPosition[1] = 1; + attackType = ESM::Weapon::AT_Thrust; + } else { movement.mPosition[1] = movement.mPosition[0] = 0; + attackType = ESM::Weapon::AT_Chop; } + } + + return attackType; +} + +void getMinMaxAttackDuration(const MWWorld::Ptr& actor, float (*fMinMaxDurations)[2]) +{ + if (!actor.getClass().hasInventoryStore(actor)) // creatures + { + fMinMaxDurations[0][0] = fMinMaxDurations[0][1] = 0.1f; + fMinMaxDurations[1][0] = fMinMaxDurations[1][1] = 0.1f; + fMinMaxDurations[2][0] = fMinMaxDurations[2][1] = 0.1f; return; } - //the more damage attackType deals the more probability it has - int slash = (weapon->mData.mSlash[0] + weapon->mData.mSlash[1])/2; - int chop = (weapon->mData.mChop[0] + weapon->mData.mChop[1])/2; - int thrust = (weapon->mData.mThrust[0] + weapon->mData.mThrust[1])/2; + // get weapon information: type and speed + const ESM::Weapon *weapon = NULL; + MWMechanics::WeaponType weaptype = MWMechanics::WeapType_None; + + MWWorld::ContainerStoreIterator weaponSlot = + MWMechanics::getActiveWeapon(actor.getClass().getCreatureStats(actor), actor.getClass().getInventoryStore(actor), &weaptype); + + float weapSpeed; + if (weaptype != MWMechanics::WeapType_HandToHand + && weaptype != MWMechanics::WeapType_Spell + && weaptype != MWMechanics::WeapType_None) + { + weapon = weaponSlot->get()->mBase; + weapSpeed = weapon->mData.mSpeed; + } + else weapSpeed = 1.0f; + + MWRender::Animation *anim = MWBase::Environment::get().getWorld()->getAnimation(actor); + + std::string weapGroup; + MWMechanics::getWeaponGroup(weaptype, weapGroup); + weapGroup = weapGroup + ": "; + + bool bRangedWeap = (weaptype >= MWMechanics::WeapType_BowAndArrow && weaptype <= MWMechanics::WeapType_Thrown); + + const char *attackType[] = {"chop ", "slash ", "thrust ", "shoot "}; + + std::string textKey = "start"; + std::string textKey2; + + // get durations for each attack type + for (int i = 0; i < (bRangedWeap ? 1 : 3); i++) + { + float start1 = anim->getTextKeyTime(weapGroup + (bRangedWeap ? attackType[3] : attackType[i]) + textKey); + + if (start1 < 0) + { + fMinMaxDurations[i][0] = fMinMaxDurations[i][1] = 0.1f; + continue; + } + + textKey2 = "min attack"; + float start2 = anim->getTextKeyTime(weapGroup + (bRangedWeap ? attackType[3] : attackType[i]) + textKey2); + + fMinMaxDurations[i][0] = (start2 - start1) / weapSpeed; + + textKey2 = "max attack"; + start1 = anim->getTextKeyTime(weapGroup + (bRangedWeap ? attackType[3] : attackType[i]) + textKey2); + + fMinMaxDurations[i][1] = fMinMaxDurations[i][0] + (start1 - start2) / weapSpeed; + } + +} - float total = slash + chop + thrust; +Ogre::Vector3 AimDirToMovingTarget(const MWWorld::Ptr& actor, const MWWorld::Ptr& target, const Ogre::Vector3& vLastTargetPos, + float duration, int weapType, float strength) +{ + float projSpeed; - float roll = static_cast(rand())/RAND_MAX; - if(roll <= static_cast(slash)/total) + // get projectile speed (depending on weapon type) + if (weapType == ESM::Weapon::MarksmanThrown) { - movement.mPosition[0] = (static_cast(rand())/RAND_MAX < 0.5f)? 1: -1; - movement.mPosition[1] = 0; + static float fThrownWeaponMinSpeed = + MWBase::Environment::get().getWorld()->getStore().get().find("fThrownWeaponMinSpeed")->getFloat(); + static float fThrownWeaponMaxSpeed = + MWBase::Environment::get().getWorld()->getStore().get().find("fThrownWeaponMaxSpeed")->getFloat(); + + projSpeed = + fThrownWeaponMinSpeed + (fThrownWeaponMaxSpeed - fThrownWeaponMinSpeed) * strength; } - else if(roll <= (static_cast(slash) + static_cast(thrust))/total) - movement.mPosition[1] = 1; else - movement.mPosition[1] = movement.mPosition[0] = 0; + { + static float fProjectileMinSpeed = + MWBase::Environment::get().getWorld()->getStore().get().find("fProjectileMinSpeed")->getFloat(); + static float fProjectileMaxSpeed = + MWBase::Environment::get().getWorld()->getStore().get().find("fProjectileMaxSpeed")->getFloat(); + + projSpeed = + fProjectileMinSpeed + (fProjectileMaxSpeed - fProjectileMinSpeed) * strength; + } + + // idea: perpendicular to dir to target speed components of target move vector and projectile vector should be the same + + Ogre::Vector3 vActorPos = Ogre::Vector3(actor.getRefData().getPosition().pos); + Ogre::Vector3 vTargetPos = Ogre::Vector3(target.getRefData().getPosition().pos); + Ogre::Vector3 vDirToTarget = vTargetPos - vActorPos; + float distToTarget = vDirToTarget.length(); + + Ogre::Vector3 vTargetMoveDir = vTargetPos - vLastTargetPos; + vTargetMoveDir /= duration; // |vTargetMoveDir| is target real speed in units/sec now + + Ogre::Vector3 vPerpToDir = vDirToTarget.crossProduct(Ogre::Vector3::UNIT_Z); + + float velPerp = vTargetMoveDir.dotProduct(vPerpToDir.normalisedCopy()); + float velDir = vTargetMoveDir.dotProduct(vDirToTarget.normalisedCopy()); + + // time to collision between target and projectile + float t_collision; + + float projVelDirSquared = projSpeed * projSpeed - velPerp * velPerp; + float projDistDiff = vDirToTarget.dotProduct(vTargetMoveDir.normalisedCopy()); + projDistDiff = sqrt(distToTarget * distToTarget - projDistDiff * projDistDiff); + + if (projVelDirSquared > 0) + t_collision = projDistDiff / (sqrt(projVelDirSquared) - velDir); + else t_collision = 0; // speed of projectile is not enough to reach moving target + + return vTargetPos + vTargetMoveDir * t_collision - vActorPos; } } diff --git a/apps/openmw/mwmechanics/aicombat.hpp b/apps/openmw/mwmechanics/aicombat.hpp index 4b728ff22..307df3872 100644 --- a/apps/openmw/mwmechanics/aicombat.hpp +++ b/apps/openmw/mwmechanics/aicombat.hpp @@ -8,12 +8,26 @@ #include "movement.hpp" #include "obstacle.hpp" +#include + #include "../mwworld/cellstore.hpp" // for Doors #include "../mwbase/world.hpp" +#include + +namespace ESM +{ + namespace AiSequence + { + struct AiCombat; + } +} + namespace MWMechanics { + class Action; + /// \brief Causes the actor to fight another actor class AiCombat : public AiPackage { @@ -22,9 +36,13 @@ namespace MWMechanics /** \param actor Actor to fight **/ AiCombat(const MWWorld::Ptr& actor); + AiCombat (const ESM::AiSequence::AiCombat* combat); + + void init(); + virtual AiCombat *clone() const; - virtual bool execute (const MWWorld::Ptr& actor,float duration); + virtual bool execute (const MWWorld::Ptr& actor, AiState& state, float duration); virtual int getTypeId() const; @@ -33,38 +51,17 @@ namespace MWMechanics ///Returns target ID MWWorld::Ptr getTarget() const; + virtual void writeState(ESM::AiSequence::AiSequence &sequence) const; + private: - PathFinder mPathFinder; - // controls duration of the actual strike - float mTimerAttack; - float mTimerReact; - // controls duration of the sideway & forward moves - // when mCombatMove is true - float mTimerCombatMove; - - // AiCombat states - bool mReadyToAttack, mAttack; - bool mFollowTarget; - bool mCombatMove; - bool mBackOffDoor; - - bool mForceNoShortcut; - ESM::Position mShortcutFailPos; - - ESM::Position mLastPos; - MWMechanics::Movement mMovement; + int mTargetActorId; - const MWWorld::CellStore* mCell; - ObstacleCheck mObstacleCheck; - float mDoorCheckDuration; - // TODO: for some reason mDoors.searchViaHandle() returns - // null pointers, workaround by keeping an iterator - MWWorld::CellRefList::List::iterator mDoorIter; - MWWorld::CellRefList& mDoors; void buildNewPath(const MWWorld::Ptr& actor, const MWWorld::Ptr& target); }; + + } #endif diff --git a/apps/openmw/mwmechanics/aicombataction.cpp b/apps/openmw/mwmechanics/aicombataction.cpp new file mode 100644 index 000000000..cc8279b1e --- /dev/null +++ b/apps/openmw/mwmechanics/aicombataction.cpp @@ -0,0 +1,544 @@ +#include "aicombataction.hpp" + +#include "../mwbase/environment.hpp" +#include "../mwbase/world.hpp" + +#include "../mwworld/class.hpp" +#include "../mwworld/esmstore.hpp" +#include "../mwworld/inventorystore.hpp" +#include "../mwworld/actionequip.hpp" + +#include "../mwmechanics/npcstats.hpp" + +#include +#include + +namespace +{ + +// RangeTypes using bitflags to allow multiple range types, as can be the case with spells having multiple effects. +enum RangeTypes +{ + Self = 0x1, + Touch = 0x10, + Target = 0x100 +}; + +int getRangeTypes (const ESM::EffectList& effects) +{ + int types = 0; + for (std::vector::const_iterator it = effects.mList.begin(); it != effects.mList.end(); ++it) + { + if (it->mRange == ESM::RT_Self) + types |= Self; + else if (it->mRange == ESM::RT_Touch) + types |= Touch; + else if (it->mRange == ESM::RT_Target) + types |= Target; + } + return types; +} + +void suggestCombatRange(int rangeTypes, float& rangeAttack, float& rangeFollow) +{ + if (rangeTypes & Touch) + { + rangeAttack = 100.f; + rangeFollow = 300.f; + } + else if (rangeTypes & Target) + { + rangeAttack = 1000.f; + rangeFollow = 0.f; + } + else + { + // For Self spells, distance doesn't matter, so back away slightly to avoid enemy hits + rangeAttack = 600.f; + rangeFollow = 0.f; + } +} + +int numEffectsToCure (const MWWorld::Ptr& actor, int effectFilter=-1) +{ + int toCure=0; + const MWMechanics::ActiveSpells& activeSpells = actor.getClass().getCreatureStats(actor).getActiveSpells(); + for (MWMechanics::ActiveSpells::TIterator it = activeSpells.begin(); it != activeSpells.end(); ++it) + { + const MWMechanics::ActiveSpells::ActiveSpellParams& params = it->second; + for (std::vector::const_iterator effectIt = params.mEffects.begin(); + effectIt != params.mEffects.end(); ++effectIt) + { + int effectId = effectIt->mEffectId; + if (effectFilter != -1 && effectId != effectFilter) + continue; + const ESM::MagicEffect* magicEffect = MWBase::Environment::get().getWorld()->getStore().get().find(effectId); + if (magicEffect->mData.mFlags & ESM::MagicEffect::Harmful + && effectIt->mDuration > 3 // Don't attempt to cure if effect runs out shortly anyway + ) + ++toCure; + } + } + return toCure; +} + +} + +namespace MWMechanics +{ + + float ratePotion (const MWWorld::Ptr &item, const MWWorld::Ptr& actor) + { + if (item.getTypeName() != typeid(ESM::Potion).name()) + return 0.f; + + const ESM::Potion* potion = item.get()->mBase; + return rateEffects(potion->mEffects, actor, MWWorld::Ptr()); + } + + float rateWeapon (const MWWorld::Ptr &item, const MWWorld::Ptr& actor, const MWWorld::Ptr& target, int type, + float arrowRating, float boltRating) + { + if (item.getTypeName() != typeid(ESM::Weapon).name()) + return 0.f; + + const ESM::Weapon* weapon = item.get()->mBase; + + if (type != -1 && weapon->mData.mType != type) + return 0.f; + + float rating=0.f; + + if (weapon->mData.mType >= ESM::Weapon::MarksmanBow) + { + rating = (weapon->mData.mChop[0] + weapon->mData.mChop[1]) / 2.f; + } + else + { + for (int i=0; i<2; ++i) + { + rating += weapon->mData.mSlash[i]; + rating += weapon->mData.mThrust[i]; + rating += weapon->mData.mChop[i]; + } + rating /= 6.f; + } + + if (item.getClass().hasItemHealth(item)) + { + if (item.getClass().getItemHealth(item) == 0) + return 0.f; + rating *= item.getClass().getItemHealth(item) / float(item.getClass().getItemMaxHealth(item)); + } + + if (weapon->mData.mType == ESM::Weapon::MarksmanBow) + { + if (arrowRating <= 0.f) + rating = 0.f; + else + rating += arrowRating; + } + else if (weapon->mData.mType == ESM::Weapon::MarksmanCrossbow) + { + if (boltRating <= 0.f) + rating = 0.f; + else + rating += boltRating; + } + + if (!weapon->mEnchant.empty()) + { + const ESM::Enchantment* enchantment = MWBase::Environment::get().getWorld()->getStore().get().find(weapon->mEnchant); + if (enchantment->mData.mType == ESM::Enchantment::WhenStrikes + && (item.getCellRef().getEnchantmentCharge() == -1 + || item.getCellRef().getEnchantmentCharge() >= enchantment->mData.mCost)) + rating += rateEffects(enchantment->mEffects, actor, target); + } + + int skill = item.getClass().getEquipmentSkill(item); + if (skill != -1) + rating *= actor.getClass().getSkill(actor, skill) / 100.f; + + return rating; + } + + float rateSpell(const ESM::Spell *spell, const MWWorld::Ptr &actor, const MWWorld::Ptr& target) + { + const CreatureStats& stats = actor.getClass().getCreatureStats(actor); + + if (spell->mData.mType != ESM::Spell::ST_Spell) + return 0.f; + + // Don't make use of racial bonus spells, like MW. Can be made optional later + if (actor.getClass().isNpc()) + { + std::string raceid = actor.get()->mBase->mRace; + const ESM::Race* race = MWBase::Environment::get().getWorld()->getStore().get().find(raceid); + if (race->mPowers.exists(spell->mId)) + return 0.f; + } + + if (spell->mData.mCost > stats.getMagicka().getCurrent()) + return 0.f; + + // Spells don't stack, so early out if the spell is still active on the target + int types = getRangeTypes(spell->mEffects); + if ((types & Self) && stats.getActiveSpells().isSpellActive(spell->mId)) + return 0.f; + if ( ((types & Touch) || (types & Target)) && target.getClass().getCreatureStats(target).getActiveSpells().isSpellActive(spell->mId)) + return 0.f; + + return rateEffects(spell->mEffects, actor, target); + } + + float rateMagicItem(const MWWorld::Ptr &ptr, const MWWorld::Ptr &actor, const MWWorld::Ptr& target) + { + if (ptr.getClass().getEnchantment(ptr).empty()) + return 0.f; + + const ESM::Enchantment* enchantment = MWBase::Environment::get().getWorld()->getStore().get().find(ptr.getClass().getEnchantment(ptr)); + if (enchantment->mData.mType == ESM::Enchantment::CastOnce) + { + return rateEffects(enchantment->mEffects, actor, target); + } + else + return 0.f; + } + + float rateEffect(const ESM::ENAMstruct &effect, const MWWorld::Ptr &actor, const MWWorld::Ptr &target) + { + // NOTE: target may be empty + + float rating = 1; + switch (effect.mEffectID) + { + case ESM::MagicEffect::Soultrap: + case ESM::MagicEffect::AlmsiviIntervention: + case ESM::MagicEffect::DivineIntervention: + case ESM::MagicEffect::CalmHumanoid: + case ESM::MagicEffect::CalmCreature: + case ESM::MagicEffect::FrenzyHumanoid: + case ESM::MagicEffect::FrenzyCreature: + case ESM::MagicEffect::DemoralizeHumanoid: + case ESM::MagicEffect::DemoralizeCreature: + case ESM::MagicEffect::RallyHumanoid: + case ESM::MagicEffect::RallyCreature: + case ESM::MagicEffect::Charm: + case ESM::MagicEffect::DetectAnimal: + case ESM::MagicEffect::DetectEnchantment: + case ESM::MagicEffect::DetectKey: + case ESM::MagicEffect::Telekinesis: + case ESM::MagicEffect::Mark: + case ESM::MagicEffect::Recall: + case ESM::MagicEffect::Jump: + case ESM::MagicEffect::WaterBreathing: + case ESM::MagicEffect::SwiftSwim: + case ESM::MagicEffect::WaterWalking: + case ESM::MagicEffect::SlowFall: + case ESM::MagicEffect::Light: + case ESM::MagicEffect::Lock: + case ESM::MagicEffect::Open: + case ESM::MagicEffect::TurnUndead: + case ESM::MagicEffect::WeaknessToCommonDisease: + case ESM::MagicEffect::WeaknessToBlightDisease: + case ESM::MagicEffect::WeaknessToCorprusDisease: + case ESM::MagicEffect::CureCommonDisease: + case ESM::MagicEffect::CureBlightDisease: + case ESM::MagicEffect::CureCorprusDisease: + case ESM::MagicEffect::Invisibility: + return 0.f; + case ESM::MagicEffect::Feather: + if (actor.getClass().getEncumbrance(actor) - actor.getClass().getCapacity(actor) >= 0) + return 100.f; + else + return 0.f; + case ESM::MagicEffect::Levitate: + return 0.f; // AI isn't designed to take advantage of this, and could be perceived as unfair anyway + case ESM::MagicEffect::BoundBoots: + case ESM::MagicEffect::BoundHelm: + if (actor.getClass().isNpc()) + { + // Beast races can't wear helmets or boots + std::string raceid = actor.get()->mBase->mRace; + const ESM::Race* race = MWBase::Environment::get().getWorld()->getStore().get().find(raceid); + if (race->mData.mFlags & ESM::Race::Beast) + return 0.f; + } + // Intended fall-through + // Creatures can not wear armor + case ESM::MagicEffect::BoundCuirass: + case ESM::MagicEffect::BoundGloves: + if (!actor.getClass().isNpc()) + return 0.f; + break; + + case ESM::MagicEffect::RestoreHealth: + case ESM::MagicEffect::RestoreMagicka: + case ESM::MagicEffect::RestoreFatigue: + if (effect.mRange == ESM::RT_Self) + { + int priority = 1; + if (effect.mEffectID == ESM::MagicEffect::RestoreHealth) + priority = 10; + const DynamicStat& current = actor.getClass().getCreatureStats(actor). + getDynamic(effect.mEffectID - ESM::MagicEffect::RestoreHealth); + float toHeal = (effect.mMagnMin + effect.mMagnMax)/2.f * effect.mDuration; + // Effect doesn't heal more than we need, *or* we are below 1/2 health + if (current.getModified() - current.getCurrent() > toHeal + || current.getCurrent() < current.getModified()*0.5) + return 10000.f * priority; + else + return -10000.f * priority; // Save for later + } + break; + + // Prefer Cure effects over Dispel, because Dispel also removes positive effects + case ESM::MagicEffect::Dispel: + return 1000.f * numEffectsToCure(actor); + case ESM::MagicEffect::CureParalyzation: + return 1001.f * numEffectsToCure(actor, ESM::MagicEffect::Paralyze); + case ESM::MagicEffect::CurePoison: + return 1001.f * numEffectsToCure(actor, ESM::MagicEffect::Poison); + + case ESM::MagicEffect::DisintegrateArmor: // TODO: check if actor is wearing armor + case ESM::MagicEffect::DisintegrateWeapon: // TODO: check if actor is wearing weapon + break; + + case ESM::MagicEffect::DamageAttribute: + case ESM::MagicEffect::DrainAttribute: + if (!target.isEmpty() && target.getClass().getCreatureStats(target).getAttribute(effect.mAttribute).getModified() <= 0) + return 0.f; + { + if (effect.mAttribute >= 0 && effect.mAttribute < ESM::Attribute::Length) + { + const float attributePriorities[ESM::Attribute::Length] = { + 1.f, // Strength + 0.5, // Intelligence + 0.6, // Willpower + 0.7, // Agility + 0.5, // Speed + 0.8, // Endurance + 0.7, // Personality + 0.3 // Luck + }; + rating *= attributePriorities[effect.mAttribute]; + } + } + break; + + case ESM::MagicEffect::DamageSkill: + case ESM::MagicEffect::DrainSkill: + if (target.isEmpty() || !target.getClass().isNpc()) + return 0.f; + if (target.getClass().getNpcStats(target).getSkill(effect.mSkill).getModified() <= 0) + return 0.f; + break; + + default: + break; + } + + // TODO: for non-cumulative effects (e.g. paralyze), check if the target is already suffering from them + + // TODO: could take into account target's resistance/weakness against the effect + + const ESM::MagicEffect* magicEffect = MWBase::Environment::get().getWorld()->getStore().get().find(effect.mEffectID); + + rating *= magicEffect->mData.mBaseCost; + + if (!(magicEffect->mData.mFlags & ESM::MagicEffect::NoMagnitude)) + rating *= (effect.mMagnMin + effect.mMagnMax)/2.f; + if (!(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration)) + rating *= effect.mDuration; + + if (magicEffect->mData.mFlags & ESM::MagicEffect::Harmful) + rating *= -1.f; + + // Currently treating all "on target" or "on touch" effects to target the enemy actor. + // Combat AI is egoistic, so doesn't consider applying positive effects to friendly actors. + if (effect.mRange != ESM::RT_Self) + rating *= -1.f; + return rating; + } + + float rateEffects(const ESM::EffectList &list, const MWWorld::Ptr& actor, const MWWorld::Ptr& target) + { + // NOTE: target may be empty + float rating = 0.f; + for (std::vector::const_iterator it = list.mList.begin(); it != list.mList.end(); ++it) + { + rating += rateEffect(*it, actor, target); + } + return rating; + } + + void ActionSpell::prepare(const MWWorld::Ptr &actor) + { + actor.getClass().getCreatureStats(actor).getSpells().setSelectedSpell(mSpellId); + actor.getClass().getCreatureStats(actor).setDrawState(DrawState_Spell); + if (actor.getClass().hasInventoryStore(actor)) + { + MWWorld::InventoryStore& inv = actor.getClass().getInventoryStore(actor); + inv.setSelectedEnchantItem(inv.end()); + } + } + + void ActionSpell::getCombatRange(float& rangeAttack, float& rangeFollow) + { + const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().find(mSpellId); + int types = getRangeTypes(spell->mEffects); + suggestCombatRange(types, rangeAttack, rangeFollow); + } + + void ActionEnchantedItem::prepare(const MWWorld::Ptr &actor) + { + actor.getClass().getCreatureStats(actor).getSpells().setSelectedSpell(std::string()); + actor.getClass().getInventoryStore(actor).setSelectedEnchantItem(mItem); + actor.getClass().getCreatureStats(actor).setDrawState(DrawState_Spell); + } + + void ActionEnchantedItem::getCombatRange(float& rangeAttack, float& rangeFollow) + { + const ESM::Enchantment* enchantment = MWBase::Environment::get().getWorld()->getStore().get().find(mItem->getClass().getEnchantment(*mItem)); + int types = getRangeTypes(enchantment->mEffects); + suggestCombatRange(types, rangeAttack, rangeFollow); + } + + void ActionPotion::getCombatRange(float& rangeAttack, float& rangeFollow) + { + // distance doesn't matter, so back away slightly to avoid enemy hits + rangeAttack = 600.f; + rangeFollow = 0.f; + } + + void ActionPotion::prepare(const MWWorld::Ptr &actor) + { + actor.getClass().apply(actor, mPotion.getCellRef().getRefId(), actor); + actor.getClass().getContainerStore(actor).remove(mPotion, 1, actor); + } + + void ActionWeapon::prepare(const MWWorld::Ptr &actor) + { + if (actor.getClass().hasInventoryStore(actor)) + { + if (mWeapon.isEmpty()) + actor.getClass().getInventoryStore(actor).unequipSlot(MWWorld::InventoryStore::Slot_CarriedRight, actor); + else + { + MWWorld::ActionEquip equip(mWeapon); + equip.execute(actor); + } + + if (!mAmmunition.isEmpty()) + { + MWWorld::ActionEquip equip(mAmmunition); + equip.execute(actor); + } + } + actor.getClass().getCreatureStats(actor).setDrawState(DrawState_Weapon); + } + + void ActionWeapon::getCombatRange(float& rangeAttack, float& rangeFollow) + { + // Already done in AiCombat itself + } + + boost::shared_ptr prepareNextAction(const MWWorld::Ptr &actor, const MWWorld::Ptr &target) + { + Spells& spells = actor.getClass().getCreatureStats(actor).getSpells(); + + float bestActionRating = 0.f; + // Default to hand-to-hand combat + boost::shared_ptr bestAction (new ActionWeapon(MWWorld::Ptr())); + + if (actor.getClass().hasInventoryStore(actor)) + { + MWWorld::InventoryStore& store = actor.getClass().getInventoryStore(actor); + + for (MWWorld::ContainerStoreIterator it = store.begin(); it != store.end(); ++it) + { + float rating = ratePotion(*it, actor); + if (rating > bestActionRating) + { + bestActionRating = rating; + bestAction.reset(new ActionPotion(*it)); + } + } + + for (MWWorld::ContainerStoreIterator it = store.begin(); it != store.end(); ++it) + { + float rating = rateMagicItem(*it, actor, target); + if (rating > bestActionRating) + { + bestActionRating = rating; + bestAction.reset(new ActionEnchantedItem(it)); + } + } + + float bestArrowRating = 0; + MWWorld::Ptr bestArrow; + for (MWWorld::ContainerStoreIterator it = store.begin(); it != store.end(); ++it) + { + float rating = rateWeapon(*it, actor, target, ESM::Weapon::Arrow); + if (rating > bestArrowRating) + { + bestArrowRating = rating; + bestArrow = *it; + } + } + + float bestBoltRating = 0; + MWWorld::Ptr bestBolt; + for (MWWorld::ContainerStoreIterator it = store.begin(); it != store.end(); ++it) + { + float rating = rateWeapon(*it, actor, target, ESM::Weapon::Bolt); + if (rating > bestBoltRating) + { + bestBoltRating = rating; + bestBolt = *it; + } + } + + for (MWWorld::ContainerStoreIterator it = store.begin(); it != store.end(); ++it) + { + std::vector equipmentSlots = it->getClass().getEquipmentSlots(*it).first; + if (std::find(equipmentSlots.begin(), equipmentSlots.end(), (int)MWWorld::InventoryStore::Slot_CarriedRight) + == equipmentSlots.end()) + continue; + + float rating = rateWeapon(*it, actor, target, -1, bestArrowRating, bestBoltRating); + if (rating > bestActionRating) + { + const ESM::Weapon* weapon = it->get()->mBase; + + MWWorld::Ptr ammo; + if (weapon->mData.mType == ESM::Weapon::MarksmanBow) + ammo = bestArrow; + else if (weapon->mData.mType == ESM::Weapon::MarksmanCrossbow) + ammo = bestBolt; + + bestActionRating = rating; + bestAction.reset(new ActionWeapon(*it, ammo)); + } + } + } + + for (Spells::TIterator it = spells.begin(); it != spells.end(); ++it) + { + const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().find(it->first); + + float rating = rateSpell(spell, actor, target); + if (rating > bestActionRating) + { + bestActionRating = rating; + bestAction.reset(new ActionSpell(spell->mId)); + } + } + + if (bestAction.get()) + bestAction->prepare(actor); + + return bestAction; + } + +} diff --git a/apps/openmw/mwmechanics/aicombataction.hpp b/apps/openmw/mwmechanics/aicombataction.hpp new file mode 100644 index 000000000..1c7451c32 --- /dev/null +++ b/apps/openmw/mwmechanics/aicombataction.hpp @@ -0,0 +1,90 @@ +#ifndef OPENMW_AICOMBAT_ACTION_H +#define OPENMW_AICOMBAT_ACTION_H + +#include + +#include "../mwworld/ptr.hpp" +#include "../mwworld/containerstore.hpp" + +#include + +namespace MWMechanics +{ + + class Action + { + public: + virtual ~Action() {} + virtual void prepare(const MWWorld::Ptr& actor) = 0; + virtual void getCombatRange (float& rangeAttack, float& rangeFollow) = 0; + virtual float getActionCooldown() { return 0.f; } + }; + + class ActionSpell : public Action + { + public: + ActionSpell(const std::string& spellId) : mSpellId(spellId) {} + std::string mSpellId; + /// Sets the given spell as selected on the actor's spell list. + virtual void prepare(const MWWorld::Ptr& actor); + + virtual void getCombatRange (float& rangeAttack, float& rangeFollow); + }; + + class ActionEnchantedItem : public Action + { + public: + ActionEnchantedItem(const MWWorld::ContainerStoreIterator& item) : mItem(item) {} + MWWorld::ContainerStoreIterator mItem; + /// Sets the given item as selected enchanted item in the actor's InventoryStore. + virtual void prepare(const MWWorld::Ptr& actor); + virtual void getCombatRange (float& rangeAttack, float& rangeFollow); + + /// Since this action has no animation, apply a small cool down for using it + virtual float getActionCooldown() { return 1.f; } + }; + + class ActionPotion : public Action + { + public: + ActionPotion(const MWWorld::Ptr& potion) : mPotion(potion) {} + MWWorld::Ptr mPotion; + /// Drinks the given potion. + virtual void prepare(const MWWorld::Ptr& actor); + virtual void getCombatRange (float& rangeAttack, float& rangeFollow); + + /// Since this action has no animation, apply a small cool down for using it + virtual float getActionCooldown() { return 1.f; } + }; + + class ActionWeapon : public Action + { + private: + MWWorld::Ptr mAmmunition; + MWWorld::Ptr mWeapon; + + public: + /// \a weapon may be empty for hand-to-hand combat + ActionWeapon(const MWWorld::Ptr& weapon, const MWWorld::Ptr& ammo = MWWorld::Ptr()) + : mWeapon(weapon), mAmmunition(ammo) {} + /// Equips the given weapon. + virtual void prepare(const MWWorld::Ptr& actor); + virtual void getCombatRange (float& rangeAttack, float& rangeFollow); + }; + + float rateSpell (const ESM::Spell* spell, const MWWorld::Ptr& actor, const MWWorld::Ptr& target); + float rateMagicItem (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor, const MWWorld::Ptr &target); + float ratePotion (const MWWorld::Ptr& item, const MWWorld::Ptr &actor); + /// @param type Skip all weapons that are not of this type (i.e. return rating 0) + float rateWeapon (const MWWorld::Ptr& item, const MWWorld::Ptr& actor, const MWWorld::Ptr& target, + int type=-1, float arrowRating=0.f, float boltRating=0.f); + + /// @note target may be empty + float rateEffect (const ESM::ENAMstruct& effect, const MWWorld::Ptr& actor, const MWWorld::Ptr& target); + /// @note target may be empty + float rateEffects (const ESM::EffectList& list, const MWWorld::Ptr& actor, const MWWorld::Ptr& target); + + boost::shared_ptr prepareNextAction (const MWWorld::Ptr& actor, const MWWorld::Ptr& target); +} + +#endif diff --git a/apps/openmw/mwmechanics/aiescort.cpp b/apps/openmw/mwmechanics/aiescort.cpp index 07bb9726e..c89cfe492 100644 --- a/apps/openmw/mwmechanics/aiescort.cpp +++ b/apps/openmw/mwmechanics/aiescort.cpp @@ -1,12 +1,15 @@ #include "aiescort.hpp" +#include + #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/class.hpp" -#include "../mwworld/timestamp.hpp" + +#include "../mwmechanics/creaturestats.hpp" #include "steering.hpp" #include "movement.hpp" @@ -20,41 +23,39 @@ namespace MWMechanics { AiEscort::AiEscort(const std::string &actorId, int duration, float x, float y, float z) - : mActorId(actorId), mX(x), mY(y), mZ(z), mDuration(duration) + : mActorId(actorId), mX(x), mY(y), mZ(z), mRemainingDuration(duration) , mCellX(std::numeric_limits::max()) , mCellY(std::numeric_limits::max()) { - mMaxDist = 470; + mMaxDist = 450; // The CS Help File states that if a duration is given, the AI package will run for that long // BUT if a location is givin, it "trumps" the duration so it will simply escort to that location. if(mX != 0 || mY != 0 || mZ != 0) - mDuration = 0; - - else - { - MWWorld::TimeStamp startTime = MWBase::Environment::get().getWorld()->getTimeStamp(); - mStartingSecond = ((startTime.getHour() - int(startTime.getHour())) * 100); - } + mRemainingDuration = 0; } AiEscort::AiEscort(const std::string &actorId, const std::string &cellId,int duration, float x, float y, float z) - : mActorId(actorId), mCellId(cellId), mX(x), mY(y), mZ(z), mDuration(duration) + : mActorId(actorId), mCellId(cellId), mX(x), mY(y), mZ(z), mRemainingDuration(duration) , mCellX(std::numeric_limits::max()) , mCellY(std::numeric_limits::max()) { - mMaxDist = 470; + mMaxDist = 450; // The CS Help File states that if a duration is given, the AI package will run for that long - // BUT if a location is givin, it "trumps" the duration so it will simply escort to that location. + // BUT if a location is given, it "trumps" the duration so it will simply escort to that location. if(mX != 0 || mY != 0 || mZ != 0) - mDuration = 0; + mRemainingDuration = 0; + } - else - { - MWWorld::TimeStamp startTime = MWBase::Environment::get().getWorld()->getTimeStamp(); - mStartingSecond = ((startTime.getHour() - int(startTime.getHour())) * 100); - } + AiEscort::AiEscort(const ESM::AiSequence::AiEscort *escort) + : mActorId(escort->mTargetId), mX(escort->mData.mX), mY(escort->mData.mY), mZ(escort->mData.mZ) + , mCellX(std::numeric_limits::max()) + , mCellY(std::numeric_limits::max()) + , mCellId(escort->mCellId) + , mRemainingDuration(escort->mRemainingDuration) + , mMaxDist(450) + { } @@ -63,18 +64,20 @@ namespace MWMechanics return new AiEscort(*this); } - bool AiEscort::execute (const MWWorld::Ptr& actor,float duration) + bool AiEscort::execute (const MWWorld::Ptr& actor, AiState& state, float duration) { // If AiEscort has ran for as long or longer then the duration specified // and the duration is not infinite, the package is complete. - if(mDuration != 0) + if(mRemainingDuration != 0) { - MWWorld::TimeStamp current = MWBase::Environment::get().getWorld()->getTimeStamp(); - unsigned int currentSecond = ((current.getHour() - int(current.getHour())) * 100); - if(currentSecond - mStartingSecond >= mDuration) + mRemainingDuration -= duration; + if (duration <= 0) return true; } + actor.getClass().getCreatureStats(actor).setDrawState(DrawState_Nothing); + actor.getClass().getCreatureStats(actor).setMovementFlag(CreatureStats::Flag_Run, false); + const MWWorld::Ptr follower = MWBase::Environment::get().getWorld()->getPtr(mActorId, false); const float* const leaderPos = actor.getRefData().getPosition().pos; const float* const followerPos = follower.getRefData().getPosition().pos; @@ -89,16 +92,20 @@ namespace MWMechanics if(distanceBetweenResult <= mMaxDist * mMaxDist) { - if(pathTo(actor,ESM::Pathgrid::Point(mX,mY,mZ),duration)) //Returns true on path complete + ESM::Pathgrid::Point point(mX,mY,mZ); + point.mAutogenerated = 0; + point.mConnectionNum = 0; + point.mUnknown = 0; + if(pathTo(actor,point,duration)) //Returns true on path complete return true; - mMaxDist = 470; + mMaxDist = 450; } else { // Stop moving if the player is to far away MWBase::Environment::get().getMechanicsManager()->playAnimationGroup(actor, "idle3", 0, 1); actor.getClass().getMovementSettings(actor).mPosition[1] = 0; - mMaxDist = 330; + mMaxDist = 250; } return false; @@ -108,5 +115,21 @@ namespace MWMechanics { return TypeIdEscort; } + + void AiEscort::writeState(ESM::AiSequence::AiSequence &sequence) const + { + std::auto_ptr escort(new ESM::AiSequence::AiEscort()); + escort->mData.mX = mX; + escort->mData.mY = mY; + escort->mData.mZ = mZ; + escort->mTargetId = mActorId; + escort->mRemainingDuration = mRemainingDuration; + escort->mCellId = mCellId; + + ESM::AiSequence::AiPackageContainer package; + package.mType = ESM::AiSequence::Ai_Escort; + package.mPackage = escort.release(); + sequence.mPackages.push_back(package); + } } diff --git a/apps/openmw/mwmechanics/aiescort.hpp b/apps/openmw/mwmechanics/aiescort.hpp index 3771417fa..f02cdba22 100644 --- a/apps/openmw/mwmechanics/aiescort.hpp +++ b/apps/openmw/mwmechanics/aiescort.hpp @@ -6,6 +6,14 @@ #include "pathfinding.hpp" +namespace ESM +{ +namespace AiSequence +{ + struct AiEscort; +} +} + namespace MWMechanics { /// \brief AI Package to have an NPC lead the player to a specific point @@ -21,12 +29,16 @@ namespace MWMechanics \implement AiEscortCell **/ AiEscort(const std::string &actorId,const std::string &cellId,int duration, float x, float y, float z); + AiEscort(const ESM::AiSequence::AiEscort* escort); + virtual AiEscort *clone() const; - virtual bool execute (const MWWorld::Ptr& actor,float duration); + virtual bool execute (const MWWorld::Ptr& actor, AiState& state, float duration); virtual int getTypeId() const; + void writeState(ESM::AiSequence::AiSequence &sequence) const; + private: std::string mActorId; std::string mCellId; @@ -34,10 +46,8 @@ namespace MWMechanics float mY; float mZ; float mMaxDist; - unsigned int mStartingSecond; - unsigned int mDuration; + float mRemainingDuration; // In seconds - PathFinder mPathFinder; int mCellX; int mCellY; }; diff --git a/apps/openmw/mwmechanics/aifollow.cpp b/apps/openmw/mwmechanics/aifollow.cpp index f1296a949..f309dc740 100644 --- a/apps/openmw/mwmechanics/aifollow.cpp +++ b/apps/openmw/mwmechanics/aifollow.cpp @@ -1,5 +1,9 @@ #include "aifollow.hpp" + #include + +#include + #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" #include "../mwworld/class.hpp" @@ -12,31 +16,53 @@ #include "steering.hpp" MWMechanics::AiFollow::AiFollow(const std::string &actorId,float duration, float x, float y, float z) -: mAlwaysFollow(false), mDuration(duration), mX(x), mY(y), mZ(z), mActorId(actorId), mCellId(""), AiPackage() +: mAlwaysFollow(false), mCommanded(false), mRemainingDuration(duration), mX(x), mY(y), mZ(z) +, mActorRefId(actorId), mCellId(""), mActorId(-1) { } MWMechanics::AiFollow::AiFollow(const std::string &actorId,const std::string &cellId,float duration, float x, float y, float z) -: mAlwaysFollow(false), mDuration(duration), mX(x), mY(y), mZ(z), mActorId(actorId), mCellId(cellId), AiPackage() +: mAlwaysFollow(false), mCommanded(false), mRemainingDuration(duration), mX(x), mY(y), mZ(z) +, mActorRefId(actorId), mCellId(cellId), mActorId(-1) { } -MWMechanics::AiFollow::AiFollow(const std::string &actorId) -: mAlwaysFollow(true), mDuration(0), mX(0), mY(0), mZ(0), mActorId(actorId), mCellId(""), AiPackage() +MWMechanics::AiFollow::AiFollow(const std::string &actorId, bool commanded) +: mAlwaysFollow(true), mCommanded(commanded), mRemainingDuration(0), mX(0), mY(0), mZ(0) +, mActorRefId(actorId), mCellId(""), mActorId(-1) { } -bool MWMechanics::AiFollow::execute (const MWWorld::Ptr& actor,float duration) +MWMechanics::AiFollow::AiFollow(const ESM::AiSequence::AiFollow *follow) + : mAlwaysFollow(follow->mAlwaysFollow), mRemainingDuration(follow->mRemainingDuration) + , mX(follow->mData.mX), mY(follow->mData.mY), mZ(follow->mData.mZ) + , mActorRefId(follow->mTargetId), mActorId(-1), mCellId(follow->mCellId) + , mCommanded(follow->mCommanded) +{ + +} + +bool MWMechanics::AiFollow::execute (const MWWorld::Ptr& actor, AiState& state, float duration) { - const MWWorld::Ptr target = MWBase::Environment::get().getWorld()->searchPtr(mActorId, false); //The target to follow + MWWorld::Ptr target = getTarget(); - if(target == MWWorld::Ptr()) return true; //Target doesn't exist + if (target.isEmpty() || !target.getRefData().getCount() || !target.getRefData().isEnabled() // Really we should be checking whether the target is currently registered + // with the MechanicsManager + ) + return true; //Target doesn't exist + + actor.getClass().getCreatureStats(actor).setDrawState(DrawState_Nothing); ESM::Position pos = actor.getRefData().getPosition(); //position of the actor if(!mAlwaysFollow) //Update if you only follow for a bit { - if(mTotalTime > mDuration && mDuration != 0) //Check if we've run out of time - return true; + //Check if we've run out of time + if (mRemainingDuration != 0) + { + mRemainingDuration -= duration; + if (duration <= 0) + return true; + } if((pos.pos[0]-mX)*(pos.pos[0]-mX) + (pos.pos[1]-mY)*(pos.pos[1]-mY) + @@ -55,7 +81,7 @@ bool MWMechanics::AiFollow::execute (const MWWorld::Ptr& actor,float duration) } } - //Set the target desition from the actor + //Set the target destination from the actor ESM::Pathgrid::Point dest = target.getRefData().getPosition().pos; if(distance(dest, pos.pos[0], pos.pos[1], pos.pos[2]) < 100) //Stop when you get close @@ -65,9 +91,9 @@ bool MWMechanics::AiFollow::execute (const MWWorld::Ptr& actor,float duration) } //Check if you're far away - if(distance(dest, pos.pos[0], pos.pos[1], pos.pos[2]) > 1000) + if(distance(dest, pos.pos[0], pos.pos[1], pos.pos[2]) > 450) actor.getClass().getCreatureStats(actor).setMovementFlag(MWMechanics::CreatureStats::Flag_Run, true); //Make NPC run - else if(distance(dest, pos.pos[0], pos.pos[1], pos.pos[2]) < 800) //Have a bit of a dead zone, otherwise npc will constantly flip between running and not when right on the edge of the running threshhold + else if(distance(dest, pos.pos[0], pos.pos[1], pos.pos[2]) < 325) //Have a bit of a dead zone, otherwise npc will constantly flip between running and not when right on the edge of the running threshhold actor.getClass().getCreatureStats(actor).setMovementFlag(MWMechanics::CreatureStats::Flag_Run, false); //make NPC walk return false; @@ -75,7 +101,7 @@ bool MWMechanics::AiFollow::execute (const MWWorld::Ptr& actor,float duration) std::string MWMechanics::AiFollow::getFollowedActor() { - return mActorId; + return mActorRefId; } MWMechanics::AiFollow *MWMechanics::AiFollow::clone() const @@ -83,7 +109,53 @@ MWMechanics::AiFollow *MWMechanics::AiFollow::clone() const return new AiFollow(*this); } - int MWMechanics::AiFollow::getTypeId() const +int MWMechanics::AiFollow::getTypeId() const { return TypeIdFollow; } + +bool MWMechanics::AiFollow::isCommanded() const +{ + return mCommanded; +} + +void MWMechanics::AiFollow::writeState(ESM::AiSequence::AiSequence &sequence) const +{ + std::auto_ptr follow(new ESM::AiSequence::AiFollow()); + follow->mData.mX = mX; + follow->mData.mY = mY; + follow->mData.mZ = mZ; + follow->mTargetId = mActorRefId; + follow->mRemainingDuration = mRemainingDuration; + follow->mCellId = mCellId; + follow->mAlwaysFollow = mAlwaysFollow; + follow->mCommanded = mCommanded; + + ESM::AiSequence::AiPackageContainer package; + package.mType = ESM::AiSequence::Ai_Follow; + package.mPackage = follow.release(); + sequence.mPackages.push_back(package); +} + +MWWorld::Ptr MWMechanics::AiFollow::getTarget() +{ + if (mActorId == -2) + return MWWorld::Ptr(); + + if (mActorId == -1) + { + MWWorld::Ptr target = MWBase::Environment::get().getWorld()->searchPtr(mActorRefId, false); + if (target.isEmpty()) + { + mActorId = -2; + return target; + } + else + mActorId = target.getClass().getCreatureStats(target).getActorId(); + } + + if (mActorId != -1) + return MWBase::Environment::get().getWorld()->searchPtrViaActorId(mActorId); + else + return MWWorld::Ptr(); +} diff --git a/apps/openmw/mwmechanics/aifollow.hpp b/apps/openmw/mwmechanics/aifollow.hpp index 10a381410..d5dd42826 100644 --- a/apps/openmw/mwmechanics/aifollow.hpp +++ b/apps/openmw/mwmechanics/aifollow.hpp @@ -6,40 +6,58 @@ #include "pathfinding.hpp" #include +namespace ESM +{ +namespace AiSequence +{ + struct AiFollow; +} +} + namespace MWMechanics { /// \brief AiPackage for an actor to follow another actor/the PC /** The AI will follow the target until a condition (time, or position) are set. Both can be disabled to cause the actor to follow the other indefinitely - **/ - class AiFollow : public AiPackage - { + **/ + class AiFollow : public AiPackage + { public: /// Follow Actor for duration or until you arrive at a world position AiFollow(const std::string &ActorId,float duration, float X, float Y, float Z); /// Follow Actor for duration or until you arrive at a position in a cell AiFollow(const std::string &ActorId,const std::string &CellId,float duration, float X, float Y, float Z); /// Follow Actor indefinitively - AiFollow(const std::string &ActorId); + AiFollow(const std::string &ActorId, bool commanded=false); + + AiFollow(const ESM::AiSequence::AiFollow* follow); + + MWWorld::Ptr getTarget(); virtual AiFollow *clone() const; - virtual bool execute (const MWWorld::Ptr& actor,float duration); + virtual bool execute (const MWWorld::Ptr& actor, AiState& state, float duration); virtual int getTypeId() const; /// Returns the actor being followed std::string getFollowedActor(); + virtual void writeState (ESM::AiSequence::AiSequence& sequence) const; + + bool isCommanded() const; + private: /// This will make the actor always follow. /** Thus ignoring mDuration and mX,mY,mZ (used for summoned creatures). **/ bool mAlwaysFollow; - float mDuration; + bool mCommanded; + float mRemainingDuration; // Seconds float mX; float mY; float mZ; - std::string mActorId; - std::string mCellId; - }; -} -#endif + std::string mActorRefId; + int mActorId; + std::string mCellId; + }; +} +#endif diff --git a/apps/openmw/mwmechanics/aipackage.cpp b/apps/openmw/mwmechanics/aipackage.cpp index 2144aa11d..f015bb8a4 100644 --- a/apps/openmw/mwmechanics/aipackage.cpp +++ b/apps/openmw/mwmechanics/aipackage.cpp @@ -16,7 +16,7 @@ MWMechanics::AiPackage::~AiPackage() {} -MWMechanics::AiPackage::AiPackage() : mLastDoorChecked(MWWorld::Ptr()), mTimer(.26), mStuckTimer(0) { //mTimer starts at .26 to force initial pathbuild +MWMechanics::AiPackage::AiPackage() : mTimer(.26), mStuckTimer(0) { //mTimer starts at .26 to force initial pathbuild } @@ -26,8 +26,6 @@ bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, ESM::Pathgrid::Po //Update various Timers mTimer += duration; //Update timer mStuckTimer += duration; //Update stuck timer - mTotalTime += duration; //Update total time following - ESM::Position pos = actor.getRefData().getPosition(); //position of the actor @@ -94,22 +92,19 @@ bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, ESM::Pathgrid::Po { /// TODO (tluppi#1#): Use ObstacleCheck here. Not working for some reason //if(mObstacleCheck.check(actor, duration)) { - if(distance(start, mStuckPos.pos[0], mStuckPos.pos[1], mStuckPos.pos[2]) < 10 && distance(dest, start) > 20) { //Actually stuck, and far enough away from destination to care + if(distance(start, mStuckPos.pos[0], mStuckPos.pos[1], mStuckPos.pos[2]) < actor.getClass().getSpeed(actor)*0.05 && distance(dest, start) > 20) { //Actually stuck, and far enough away from destination to care // first check if we're walking into a door MWWorld::Ptr door = getNearbyDoor(actor); if(door != MWWorld::Ptr()) // NOTE: checks interior cells only { - if(door.getCellRef().getTrap().empty() && mLastDoorChecked != door) { //Open the door if untrapped - door.getClass().activate(door, actor).get()->execute(actor); - mLastDoorChecked = door; + if(!door.getCellRef().getTeleport() && door.getCellRef().getTrap().empty() && door.getClass().getDoorState(door) == 0) { //Open the door if untrapped + MWBase::Environment::get().getWorld()->activateDoor(door, 1); } } else // probably walking into another NPC { - // TODO: diagonal should have same animation as walk forward - // but doesn't seem to do that? actor.getClass().getMovementSettings(actor).mPosition[0] = 1; - actor.getClass().getMovementSettings(actor).mPosition[1] = 0.1f; + actor.getClass().getMovementSettings(actor).mPosition[1] = 1; // change the angle a bit, too zTurn(actor, Ogre::Degree(mPathFinder.getZAngleToNext(pos.pos[0] + 1, pos.pos[1]))); } @@ -117,7 +112,6 @@ bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, ESM::Pathgrid::Po else { //Not stuck, so reset things mStuckTimer = 0; mStuckPos = pos; - mLastDoorChecked = MWWorld::Ptr(); //Resets it, in case he gets stuck behind the door again actor.getClass().getMovementSettings(actor).mPosition[1] = 1; //Just run forward } } diff --git a/apps/openmw/mwmechanics/aipackage.hpp b/apps/openmw/mwmechanics/aipackage.hpp index 055958384..df970f801 100644 --- a/apps/openmw/mwmechanics/aipackage.hpp +++ b/apps/openmw/mwmechanics/aipackage.hpp @@ -6,14 +6,25 @@ #include "../mwbase/world.hpp" #include "obstacle.hpp" +#include "aistate.hpp" namespace MWWorld { class Ptr; } +namespace ESM +{ + namespace AiSequence + { + class AiSequence; + } +} + + namespace MWMechanics { + /// \brief Base class for AI packages class AiPackage { @@ -42,7 +53,7 @@ namespace MWMechanics /// Updates and runs the package (Should run every frame) /// \return Package completed? - virtual bool execute (const MWWorld::Ptr& actor,float duration) = 0; + virtual bool execute (const MWWorld::Ptr& actor, AiState& state, float duration) = 0; /// Returns the TypeID of the AiPackage /// \see enum TypeId @@ -51,6 +62,8 @@ namespace MWMechanics /// Higher number is higher priority (0 being the lowest) virtual unsigned int getPriority() const {return 0;} + virtual void writeState (ESM::AiSequence::AiSequence& sequence) const {} + protected: /// Causes the actor to attempt to walk to the specified location /** \return If the actor has arrived at his destination **/ @@ -59,13 +72,9 @@ namespace MWMechanics PathFinder mPathFinder; ObstacleCheck mObstacleCheck; - float mDoorCheckDuration; - float mTimer; - float mStuckTimer; - float mTotalTime; + float mTimer; + float mStuckTimer; - MWWorld::Ptr mLastDoorChecked; //Used to ensure we don't try to CONSTANTLY open a door - ESM::Position mStuckPos; ESM::Pathgrid::Point mPrevDest; }; diff --git a/apps/openmw/mwmechanics/aipursue.cpp b/apps/openmw/mwmechanics/aipursue.cpp index cd9d34e08..0c3de9643 100644 --- a/apps/openmw/mwmechanics/aipursue.cpp +++ b/apps/openmw/mwmechanics/aipursue.cpp @@ -1,5 +1,7 @@ #include "aipursue.hpp" +#include + #include "../mwbase/environment.hpp" #include "../mwworld/class.hpp" @@ -18,18 +20,34 @@ AiPursue::AiPursue(const MWWorld::Ptr& actor) : mTargetActorId(actor.getClass().getCreatureStats(actor).getActorId()) { } + +AiPursue::AiPursue(const ESM::AiSequence::AiPursue *pursue) + : mTargetActorId(pursue->mTargetActorId) +{ +} + AiPursue *MWMechanics::AiPursue::clone() const { return new AiPursue(*this); } -bool AiPursue::execute (const MWWorld::Ptr& actor, float duration) +bool AiPursue::execute (const MWWorld::Ptr& actor, AiState& state, float duration) { + if(actor.getClass().getCreatureStats(actor).isDead()) + return true; + ESM::Position pos = actor.getRefData().getPosition(); //position of the actor const MWWorld::Ptr target = MWBase::Environment::get().getWorld()->searchPtrViaActorId(mTargetActorId); //The target to follow - if(target == MWWorld::Ptr()) + if(target == MWWorld::Ptr() || !target.getRefData().getCount() || !target.getRefData().isEnabled() // Really we should be checking whether the target is currently registered + // with the MechanicsManager + ) return true; //Target doesn't exist + if(target.getClass().getCreatureStats(target).isDead()) + return true; + + actor.getClass().getCreatureStats(actor).setDrawState(DrawState_Nothing); + //Set the target desition from the actor ESM::Pathgrid::Point dest = target.getRefData().getPosition().pos; @@ -57,4 +75,15 @@ MWWorld::Ptr AiPursue::getTarget() const return MWBase::Environment::get().getWorld()->searchPtrViaActorId(mTargetActorId); } +void AiPursue::writeState(ESM::AiSequence::AiSequence &sequence) const +{ + std::auto_ptr pursue(new ESM::AiSequence::AiPursue()); + pursue->mTargetActorId = mTargetActorId; + + ESM::AiSequence::AiPackageContainer package; + package.mType = ESM::AiSequence::Ai_Pursue; + package.mPackage = pursue.release(); + sequence.mPackages.push_back(package); +} + } // namespace MWMechanics diff --git a/apps/openmw/mwmechanics/aipursue.hpp b/apps/openmw/mwmechanics/aipursue.hpp index 27affc9ac..493a27985 100644 --- a/apps/openmw/mwmechanics/aipursue.hpp +++ b/apps/openmw/mwmechanics/aipursue.hpp @@ -7,6 +7,14 @@ #include "pathfinding.hpp" +namespace ESM +{ +namespace AiSequence +{ + struct AiPursue; +} +} + namespace MWMechanics { /// \brief Makes the actor very closely follow the actor @@ -20,17 +28,19 @@ namespace MWMechanics /** \param actor Actor to pursue **/ AiPursue(const MWWorld::Ptr& actor); + AiPursue(const ESM::AiSequence::AiPursue* pursue); + virtual AiPursue *clone() const; - virtual bool execute (const MWWorld::Ptr& actor,float duration); + virtual bool execute (const MWWorld::Ptr& actor, AiState& state, float duration); virtual int getTypeId() const; MWWorld::Ptr getTarget() const; + virtual void writeState (ESM::AiSequence::AiSequence& sequence) const; + private: int mTargetActorId; // The actor to pursue - int mCellX; - int mCellY; }; } #endif diff --git a/apps/openmw/mwmechanics/aisequence.cpp b/apps/openmw/mwmechanics/aisequence.cpp index 8a6a69b27..990145c8d 100644 --- a/apps/openmw/mwmechanics/aisequence.cpp +++ b/apps/openmw/mwmechanics/aisequence.cpp @@ -2,6 +2,7 @@ #include "aisequence.hpp" #include "aipackage.hpp" +#include "aistate.hpp" #include "aiwander.hpp" #include "aiescort.hpp" @@ -11,6 +12,8 @@ #include "aicombat.hpp" #include "aipursue.hpp" +#include + #include "../mwworld/class.hpp" #include "creaturestats.hpp" #include "npcstats.hpp" @@ -29,9 +32,11 @@ void AiSequence::copy (const AiSequence& sequence) AiSequence::AiSequence() : mDone (false), mLastAiPackage(-1) {} -AiSequence::AiSequence (const AiSequence& sequence) : mDone (false) +AiSequence::AiSequence (const AiSequence& sequence) { copy (sequence); + mDone = sequence.mDone; + mLastAiPackage = sequence.mLastAiPackage; } AiSequence& AiSequence::operator= (const AiSequence& sequence) @@ -41,6 +46,7 @@ AiSequence& AiSequence::operator= (const AiSequence& sequence) clear(); copy (sequence); mDone = sequence.mDone; + mLastAiPackage = sequence.mLastAiPackage; } return *this; @@ -70,31 +76,52 @@ bool AiSequence::getCombatTarget(MWWorld::Ptr &targetActor) const return true; } -bool AiSequence::canAddTarget(const ESM::Position& actorPos, float distToTarget) const +std::list::const_iterator AiSequence::begin() const +{ + return mPackages.begin(); +} + +std::list::const_iterator AiSequence::end() const { - bool firstCombatFound = false; - MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); + return mPackages.end(); +} + +void AiSequence::erase(std::list::const_iterator package) +{ + // Not sure if manually terminated packages should trigger mDone, probably not? + for(std::list::iterator it = mPackages.begin(); it != mPackages.end(); ++it) + { + if (package == it) + { + mPackages.erase(it); + return; + } + } + throw std::runtime_error("can't find package to erase"); +} +bool AiSequence::isInCombat() const +{ for(std::list::const_iterator it = mPackages.begin(); it != mPackages.end(); ++it) { if ((*it)->getTypeId() == AiPackage::TypeIdCombat) - { - firstCombatFound = true; + return true; + } + return false; +} +bool AiSequence::isInCombat(const MWWorld::Ptr &actor) const +{ + for(std::list::const_iterator it = mPackages.begin(); it != mPackages.end(); ++it) + { + if ((*it)->getTypeId() == AiPackage::TypeIdCombat) + { const AiCombat *combat = static_cast(*it); - if (combat->getTarget() != player ) return false; // only 1 non-player target allowed - else - { - // add new target only if current target (player) is farther - ESM::Position &targetPos = combat->getTarget().getRefData().getPosition(); - - float distToCurrTarget = (Ogre::Vector3(targetPos.pos) - Ogre::Vector3(actorPos.pos)).length(); - return (distToCurrTarget > distToTarget); - } + if (combat->getTarget() == actor) + return true; } - else if (firstCombatFound) break; // assumes combat packages go one-by-one in packages list } - return true; + return false; } void AiSequence::stopCombat() @@ -120,9 +147,10 @@ bool AiSequence::isPackageDone() const return mDone; } -void AiSequence::execute (const MWWorld::Ptr& actor,float duration) +void AiSequence::execute (const MWWorld::Ptr& actor, AiState& state,float duration) { - if(actor != MWBase::Environment::get().getWorld()->getPlayerPtr()) + if(actor != MWBase::Environment::get().getWorld()->getPlayerPtr() + && !actor.getClass().getCreatureStats(actor).getKnockedDown()) { if (!mPackages.empty()) { @@ -151,7 +179,7 @@ void AiSequence::execute (const MWWorld::Ptr& actor,float duration) } else { - ESM::Position &targetPos = target.getRefData().getPosition(); + const ESM::Position &targetPos = target.getRefData().getPosition(); float distTo = (Ogre::Vector3(targetPos.pos) - vActorPos).length(); if (distTo < nearestDist) @@ -181,7 +209,7 @@ void AiSequence::execute (const MWWorld::Ptr& actor,float duration) } } - if (package->execute (actor,duration)) + if (package->execute (actor,state,duration)) { // To account for the rare case where AiPackage::execute() queued another AI package // (e.g. AiPursue executing a dialogue script that uses startCombat) @@ -209,6 +237,9 @@ void AiSequence::clear() void AiSequence::stack (const AiPackage& package, const MWWorld::Ptr& actor) { + if (actor == MWBase::Environment::get().getWorld()->getPlayerPtr()) + throw std::runtime_error("Can't add AI packages to player"); + if (package.getTypeId() == AiPackage::TypeIdCombat || package.getTypeId() == AiPackage::TypeIdPursue) { // Notify AiWander of our current position so we can return to it after combat finished @@ -219,6 +250,11 @@ void AiSequence::stack (const AiPackage& package, const MWWorld::Ptr& actor) { return; // target is already pursued } + if((*iter)->getTypeId() == AiPackage::TypeIdCombat && package.getTypeId() == AiPackage::TypeIdCombat + && static_cast(*iter)->getTarget() == static_cast(&package)->getTarget()) + { + return; // already in combat with this actor + } else if ((*iter)->getTypeId() == AiPackage::TypeIdWander) static_cast(*iter)->setReturnPosition(Ogre::Vector3(actor.getRefData().getPosition().pos)); } @@ -226,20 +262,14 @@ void AiSequence::stack (const AiPackage& package, const MWWorld::Ptr& actor) for(std::list::iterator it = mPackages.begin(); it != mPackages.end(); ++it) { - if(mPackages.front()->getPriority() <= package.getPriority()) + if((*it)->getPriority() <= package.getPriority()) { mPackages.insert(it,package.clone()); return; } } - if(mPackages.empty()) - mPackages.push_front (package.clone()); -} - -void AiSequence::queue (const AiPackage& package) -{ - mPackages.push_back (package.clone()); + mPackages.push_front (package.clone()); } AiPackage* MWMechanics::AiSequence::getActivePackage() @@ -258,7 +288,8 @@ void AiSequence::fill(const ESM::AIPackageList &list) if (it->mType == ESM::AI_Wander) { ESM::AIWander data = it->mWander; - std::vector idles; + std::vector idles; + idles.reserve(8); for (int i=0; i<8; ++i) idles.push_back(data.mIdle[i]); package = new MWMechanics::AiWander(data.mDistance, data.mDuration, data.mTimeOfDay, idles, data.mShouldRepeat); @@ -287,4 +318,77 @@ void AiSequence::fill(const ESM::AIPackageList &list) } } +void AiSequence::writeState(ESM::AiSequence::AiSequence &sequence) const +{ + for (std::list::const_iterator iter (mPackages.begin()); iter!=mPackages.end(); ++iter) + { + (*iter)->writeState(sequence); + } +} + +void AiSequence::readState(const ESM::AiSequence::AiSequence &sequence) +{ + if (!sequence.mPackages.empty()) + clear(); + + for (std::vector::const_iterator it = sequence.mPackages.begin(); + it != sequence.mPackages.end(); ++it) + { + switch (it->mType) + { + case ESM::AiSequence::Ai_Wander: + { + MWMechanics::AiWander* wander = new AiWander( + dynamic_cast(it->mPackage)); + mPackages.push_back(wander); + break; + } + case ESM::AiSequence::Ai_Travel: + { + MWMechanics::AiTravel* travel = new AiTravel( + dynamic_cast(it->mPackage)); + mPackages.push_back(travel); + break; + } + case ESM::AiSequence::Ai_Escort: + { + MWMechanics::AiEscort* escort = new AiEscort( + dynamic_cast(it->mPackage)); + mPackages.push_back(escort); + break; + } + case ESM::AiSequence::Ai_Follow: + { + MWMechanics::AiFollow* follow = new AiFollow( + dynamic_cast(it->mPackage)); + mPackages.push_back(follow); + break; + } + case ESM::AiSequence::Ai_Activate: + { + MWMechanics::AiActivate* activate = new AiActivate( + dynamic_cast(it->mPackage)); + mPackages.push_back(activate); + break; + } + case ESM::AiSequence::Ai_Combat: + { + MWMechanics::AiCombat* combat = new AiCombat( + dynamic_cast(it->mPackage)); + mPackages.push_back(combat); + break; + } + case ESM::AiSequence::Ai_Pursue: + { + MWMechanics::AiPursue* pursue = new AiPursue( + dynamic_cast(it->mPackage)); + mPackages.push_back(pursue); + break; + } + default: + break; + } + } +} + } // namespace MWMechanics diff --git a/apps/openmw/mwmechanics/aisequence.hpp b/apps/openmw/mwmechanics/aisequence.hpp index 41a280da8..25605ff44 100644 --- a/apps/openmw/mwmechanics/aisequence.hpp +++ b/apps/openmw/mwmechanics/aisequence.hpp @@ -4,15 +4,30 @@ #include #include +//#include "aistate.hpp" namespace MWWorld { class Ptr; } +namespace ESM +{ + namespace AiSequence + { + class AiSequence; + } +} + + + namespace MWMechanics { class AiPackage; + + template< class Base > class DerivedClassStorage; + struct AiTemporaryBase; + typedef DerivedClassStorage AiState; /// \brief Sequence of AI-packages for a single actor /** The top-most AI package is run each frame. When completed, it is removed from the stack. **/ @@ -42,6 +57,12 @@ namespace MWMechanics virtual ~AiSequence(); + /// Iterator may be invalidated by any function calls other than begin() or end(). + std::list::const_iterator begin() const; + std::list::const_iterator end() const; + + void erase (std::list::const_iterator package); + /// Returns currently executing AiPackage type /** \see enum AiPackage::TypeId **/ int getTypeId() const; @@ -55,6 +76,12 @@ namespace MWMechanics /// Return true and assign target if combat package is currently active, return false otherwise bool getCombatTarget (MWWorld::Ptr &targetActor) const; + /// Is there any combat package? + bool isInCombat () const; + + /// Are we in combat with this particular actor? + bool isInCombat (const MWWorld::Ptr& actor) const; + bool canAddTarget(const ESM::Position& actorPos, float distToTarget) const; ///< Function assumes that actor can have only 1 target apart player @@ -68,7 +95,7 @@ namespace MWMechanics void stopPursuit(); /// Execute current package, switching if needed. - void execute (const MWWorld::Ptr& actor,float duration); + void execute (const MWWorld::Ptr& actor, MWMechanics::AiState& state, float duration); /// Remove all packages. void clear(); @@ -78,10 +105,6 @@ namespace MWMechanics @param actor The actor that owns this AiSequence **/ void stack (const AiPackage& package, const MWWorld::Ptr& actor); - /// Add \a package to the end of the sequence - /** Executed after all other packages have been completed **/ - void queue (const AiPackage& package); - /// Return the current active package. /** If there is no active package, it will throw an exception **/ AiPackage* getActivePackage(); @@ -90,6 +113,9 @@ namespace MWMechanics /** Typically used for loading from the ESM \see ESM::AIPackageList **/ void fill (const ESM::AIPackageList& list); + + void writeState (ESM::AiSequence::AiSequence& sequence) const; + void readState (const ESM::AiSequence::AiSequence& sequence); }; } diff --git a/apps/openmw/mwmechanics/aistate.hpp b/apps/openmw/mwmechanics/aistate.hpp new file mode 100644 index 000000000..7b670ad47 --- /dev/null +++ b/apps/openmw/mwmechanics/aistate.hpp @@ -0,0 +1,136 @@ +#ifndef AISTATE_H +#define AISTATE_H + +#include +#include + +// c++11 replacement +#include +#include + +namespace MWMechanics +{ + + /** \brief stores one object of any class derived from Base. + * Requesting a certain dereived class via get() either returns + * the stored object if it has the correct type or otherwise replaces + * it with an object of the requested type. + */ + template< class Base > + class DerivedClassStorage + { + private: + Base* mStorage; + + // assert that Derived is derived from Base. + template< class Derived > + void assert_derived() + { + // c++11: + // static_assert( std::is_base_of , "DerivedClassStorage may only store derived classes" ); + + // boost: + BOOST_STATIC_ASSERT((boost::is_base_of::value));//,"DerivedClassStorage may only store derived classes"); + } + + //if needed you have to provide a clone member function + DerivedClassStorage( const DerivedClassStorage& other ); + DerivedClassStorage& operator=( const DerivedClassStorage& ); + + public: + /// \brief returns reference to stored object or deletes it and creates a fitting + template< class Derived > + Derived& get() + { + assert_derived(); + + Derived* result = dynamic_cast(mStorage); + + if(!result) + { + if(mStorage) + delete mStorage; + mStorage = result = new Derived(); + } + + //return a reference to the (new allocated) object + return *result; + } + + template< class Derived > + void store( const Derived& payload ) + { + assert_derived(); + if(mStorage) + delete mStorage; + mStorage = new Derived(payload); + } + + /// \brief takes ownership of the passed object + template< class Derived > + void moveIn( Derived* p ) + { + assert_derived(); + if(mStorage) + delete mStorage; + mStorage = p; + } + + /// \brief gives away ownership of object. Throws exception if storage does not contain Derived or is empty. + template< class Derived > + Derived* moveOut() + { + assert_derived(); + + + if(!mStorage) + throw std::runtime_error("Cant move out: empty storage."); + + Derived* result = dynamic_cast(mStorage); + + if(!mStorage) + throw std::runtime_error("Cant move out: wrong type requested."); + + return result; + } + + bool empty() const + { + return mStorage == NULL; + } + + const std::type_info& getType() const + { + return typeid(mStorage); + } + + + DerivedClassStorage():mStorage(NULL){}; + ~DerivedClassStorage() + { + if(mStorage) + delete mStorage; + }; + + + + }; + + + /// \brief base class for the temporary storage of AiPackages. + /** + * Each AI package with temporary values needs a AiPackageStorage class + * which is derived from AiTemporaryBase. The CharacterController holds a container + * AiState where one of these storages can be stored at a time. + * The execute(...) member function takes this container as an argument. + * */ + struct AiTemporaryBase + { + virtual ~AiTemporaryBase(){}; + }; + + /// \brief Container for AI package status. + typedef DerivedClassStorage AiState; +} + +#endif // AISTATE_H diff --git a/apps/openmw/mwmechanics/aitravel.cpp b/apps/openmw/mwmechanics/aitravel.cpp index 024656b38..959784983 100644 --- a/apps/openmw/mwmechanics/aitravel.cpp +++ b/apps/openmw/mwmechanics/aitravel.cpp @@ -1,5 +1,9 @@ #include "aitravel.hpp" +#include + +#include + #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" @@ -13,18 +17,26 @@ namespace MWMechanics { AiTravel::AiTravel(float x, float y, float z) - : mX(x),mY(y),mZ(z),mPathFinder() + : mX(x),mY(y),mZ(z) , mCellX(std::numeric_limits::max()) , mCellY(std::numeric_limits::max()) { } + AiTravel::AiTravel(const ESM::AiSequence::AiTravel *travel) + : mX(travel->mData.mX), mY(travel->mData.mY), mZ(travel->mData.mZ) + , mCellX(std::numeric_limits::max()) + , mCellY(std::numeric_limits::max()) + { + + } + AiTravel *MWMechanics::AiTravel::clone() const { return new AiTravel(*this); } - bool AiTravel::execute (const MWWorld::Ptr& actor,float duration) + bool AiTravel::execute (const MWWorld::Ptr& actor, AiState& state, float duration) { MWBase::World *world = MWBase::Environment::get().getWorld(); ESM::Position pos = actor.getRefData().getPosition(); @@ -33,6 +45,8 @@ namespace MWMechanics actor.getClass().getCreatureStats(actor).setMovementFlag(CreatureStats::Flag_Run, false); + actor.getClass().getCreatureStats(actor).setDrawState(DrawState_Nothing); + MWWorld::Ptr player = world->getPlayerPtr(); if(cell->mData.mX != player.getCell()->getCell()->mData.mX) { @@ -57,6 +71,12 @@ namespace MWMechanics } } + // Maximum travel distance for vanilla compatibility. + // Was likely meant to prevent NPCs walking into non-loaded exterior cells, but for some reason is used in interior cells as well. + // We can make this configurable at some point, but the default *must* be the below value. Anything else will break shoddily-written content (*cough* MW *cough*) in bizarre ways. + if (Ogre::Vector3(mX, mY, mZ).squaredDistance(Ogre::Vector3(pos.pos)) > 7168*7168) + return false; + bool cellChange = cell->mData.mX != mCellX || cell->mData.mY != mCellY; if(!mPathFinder.isPathConstructed() || cellChange) { @@ -92,5 +112,18 @@ namespace MWMechanics { return TypeIdTravel; } + + void AiTravel::writeState(ESM::AiSequence::AiSequence &sequence) const + { + std::auto_ptr travel(new ESM::AiSequence::AiTravel()); + travel->mData.mX = mX; + travel->mData.mY = mY; + travel->mData.mZ = mZ; + + ESM::AiSequence::AiPackageContainer package; + package.mType = ESM::AiSequence::Ai_Travel; + package.mPackage = travel.release(); + sequence.mPackages.push_back(package); + } } diff --git a/apps/openmw/mwmechanics/aitravel.hpp b/apps/openmw/mwmechanics/aitravel.hpp index ea7f1dc32..c2c33c2cf 100644 --- a/apps/openmw/mwmechanics/aitravel.hpp +++ b/apps/openmw/mwmechanics/aitravel.hpp @@ -5,17 +5,29 @@ #include "pathfinding.hpp" +namespace ESM +{ +namespace AiSequence +{ + struct AiTravel; +} +} + namespace MWMechanics -{ +{ /// \brief Causes the AI to travel to the specified point class AiTravel : public AiPackage { - public: + public: /// Default constructor AiTravel(float x, float y, float z); + AiTravel(const ESM::AiSequence::AiTravel* travel); + + void writeState(ESM::AiSequence::AiSequence &sequence) const; + virtual AiTravel *clone() const; - virtual bool execute (const MWWorld::Ptr& actor,float duration); + virtual bool execute (const MWWorld::Ptr& actor, AiState& state, float duration); virtual int getTypeId() const; @@ -27,7 +39,6 @@ namespace MWMechanics int mCellX; int mCellY; - PathFinder mPathFinder; }; } diff --git a/apps/openmw/mwmechanics/aiwander.cpp b/apps/openmw/mwmechanics/aiwander.cpp index 1c870bda4..a70200833 100644 --- a/apps/openmw/mwmechanics/aiwander.cpp +++ b/apps/openmw/mwmechanics/aiwander.cpp @@ -3,6 +3,8 @@ #include #include +#include + #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" @@ -16,36 +18,83 @@ #include "steering.hpp" #include "movement.hpp" + + namespace MWMechanics { static const int COUNT_BEFORE_RESET = 200; // TODO: maybe no longer needed static const float DOOR_CHECK_INTERVAL = 1.5f; + static const float REACTION_INTERVAL = 0.25f; + static const int GREETING_SHOULD_START = 4; //how many reaction intervals should pass before NPC can greet player + static const int GREETING_SHOULD_END = 10; - AiWander::AiWander(int distance, int duration, int timeOfDay, const std::vector& idle, bool repeat): + /// \brief This class holds the variables AiWander needs which are deleted if the package becomes inactive. + struct AiWanderStorage : AiTemporaryBase + { + // the z rotation angle (degrees) we want to reach + // used every frame when mRotate is true + Ogre::Radian mTargetAngle; + bool mRotate; + float mReaction; // update some actions infrequently + + + AiWander::GreetingState mSaidGreeting; + int mGreetingTimer; + + // Cached current cell location + int mCellX; + int mCellY; + // Cell location multiplied by ESM::Land::REAL_SIZE + float mXCell; + float mYCell; + + const MWWorld::CellStore* mCell; // for detecting cell change + + // AiWander states + bool mChooseAction; + bool mIdleNow; + bool mMoveNow; + bool mWalking; + + unsigned short mPlayedIdle; + + AiWanderStorage(): + mTargetAngle(0), + mRotate(false), + mReaction(0), + mSaidGreeting(AiWander::Greet_None), + mGreetingTimer(0), + mCellX(std::numeric_limits::max()), + mCellY(std::numeric_limits::max()), + mXCell(0), + mYCell(0), + mCell(NULL), + mChooseAction(true), + mIdleNow(false), + mMoveNow(false), + mWalking(false), + mPlayedIdle(0) + {}; + }; + + AiWander::AiWander(int distance, int duration, int timeOfDay, const std::vector& idle, bool repeat): mDistance(distance), mDuration(duration), mTimeOfDay(timeOfDay), mIdle(idle), mRepeat(repeat) - , mCellX(std::numeric_limits::max()) - , mCellY(std::numeric_limits::max()) - , mXCell(0) - , mYCell(0) - , mCell(NULL) - , mStuckCount(0) // TODO: maybe no longer needed - , mDoorCheckDuration(0) - , mTrimCurrentNode(false) - , mReaction(0) - , mGreetDistanceMultiplier(0) - , mGreetDistanceReset(0) - , mChance(0) - , mRotate(false) - , mTargetAngle(0) - , mSaidGreeting(false) - , mHasReturnPosition(false) - , mReturnPosition(0,0,0) { - for(unsigned short counter = 0; counter < mIdle.size(); counter++) - { - if(mIdle[counter] >= 127 || mIdle[counter] < 0) - mIdle[counter] = 0; - } + mIdle.resize(8, 0); + init(); + } + + void AiWander::init() + { + // NOTE: mDistance and mDuration must be set already + + + mStuckCount = 0;// TODO: maybe no longer needed + mDoorCheckDuration = 0; + mTrimCurrentNode = false; + + mHasReturnPosition = false; + mReturnPosition = Ogre::Vector3(0,0,0); if(mDistance < 0) mDistance = 0; @@ -55,22 +104,9 @@ namespace MWMechanics mTimeOfDay = 0; mStartTime = MWBase::Environment::get().getWorld()->getTimeStamp(); - mPlayedIdle = 0; - const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); - mIdleChanceMultiplier = - store.get().find("fIdleChanceMultiplier")->getFloat(); - - mGreetDistanceMultiplier = - store.get().find("iGreetDistanceMultiplier")->getInt(); - mGreetDistanceReset = - store.get().find("fGreetDistanceReset")->getFloat(); - mChance = store.get().find("fVoiceIdleOdds")->getFloat(); mStoredAvailableNodes = false; - mChooseAction = true; - mIdleNow = false; - mMoveNow = false; - mWalking = false; + } AiPackage * MWMechanics::AiWander::clone() const @@ -128,42 +164,65 @@ namespace MWMechanics * actors will enter combat (i.e. no longer wandering) and different pathfinding * will kick in. */ - bool AiWander::execute (const MWWorld::Ptr& actor,float duration) + bool AiWander::execute (const MWWorld::Ptr& actor, AiState& state, float duration) { + // get or create temporary storage + AiWanderStorage& storage = state.get(); + + + const MWWorld::CellStore*& currentCell = storage.mCell; MWMechanics::CreatureStats& cStats = actor.getClass().getCreatureStats(actor); if(cStats.isDead() || cStats.getHealth().getCurrent() <= 0) return true; // Don't bother with dead actors - bool cellChange = mCell && (actor.getCell() != mCell); - if(!mCell || cellChange) + bool cellChange = currentCell && (actor.getCell() != currentCell); + if(!currentCell || cellChange) { - mCell = actor.getCell(); + currentCell = actor.getCell(); mStoredAvailableNodes = false; // prob. not needed since mDistance = 0 } - const ESM::Cell *cell = mCell->getCell(); + const ESM::Cell *cell = currentCell->getCell(); cStats.setDrawState(DrawState_Nothing); cStats.setMovementFlag(CreatureStats::Flag_Run, false); ESM::Position pos = actor.getRefData().getPosition(); - + + + bool& idleNow = storage.mIdleNow; + bool& moveNow = storage.mMoveNow; + bool& walking = storage.mWalking; // Check if an idle actor is too close to a door - if so start walking mDoorCheckDuration += duration; if(mDoorCheckDuration >= DOOR_CHECK_INTERVAL) { mDoorCheckDuration = 0; // restart timer if(mDistance && // actor is not intended to be stationary - mIdleNow && // but is in idle - !mWalking && // FIXME: some actors are idle while walking + idleNow && // but is in idle + !walking && // FIXME: some actors are idle while walking proximityToDoor(actor, MIN_DIST_TO_DOOR_SQUARED*1.6*1.6)) // NOTE: checks interior cells only { - mIdleNow = false; - mMoveNow = true; + idleNow = false; + moveNow = true; mTrimCurrentNode = false; // just in case } } - if(mWalking) // have not yet reached the destination + // Are we there yet? + bool& chooseAction = storage.mChooseAction; + if(walking && + mPathFinder.checkPathCompleted(pos.pos[0], pos.pos[1], pos.pos[2])) + { + stopWalking(actor); + moveNow = false; + walking = false; + chooseAction = true; + mHasReturnPosition = false; + } + + + + if(walking) // have not yet reached the destination { // turn towards the next point in mPath zTurn(actor, Ogre::Degree(mPathFinder.getZAngleToNext(pos.pos[0], pos.pos[1]))); @@ -180,8 +239,8 @@ namespace MWMechanics trimAllowedNodes(mAllowedNodes, mPathFinder); mObstacleCheck.clear(); mPathFinder.clearPath(); - mWalking = false; - mMoveNow = true; + walking = false; + moveNow = true; } else // probably walking into another NPC { @@ -202,30 +261,35 @@ namespace MWMechanics mObstacleCheck.clear(); stopWalking(actor); - mMoveNow = false; - mWalking = false; - mChooseAction = true; + moveNow = false; + walking = false; + chooseAction = true; } //#endif } - - if (mRotate) + + + Ogre::Radian& targetAngle = storage.mTargetAngle; + bool& rotate = storage.mRotate; + if (rotate) { // Reduce the turning animation glitch by using a *HUGE* value of // epsilon... TODO: a proper fix might be in either the physics or the // animation subsystem - if (zTurn(actor, Ogre::Degree(mTargetAngle), Ogre::Degree(5))) - mRotate = false; + if (zTurn(actor, targetAngle, Ogre::Degree(5))) + rotate = false; } - mReaction += duration; - if(mReaction > 0.25f) // FIXME: hard coded constant + float& lastReaction = storage.mReaction; + lastReaction += duration; + if(lastReaction < REACTION_INTERVAL) { - mReaction = 0; return false; } + else + lastReaction = 0; - // NOTE: everything below get updated every 0.25 seconds + // NOTE: everything below get updated every REACTION_INTERVAL seconds MWBase::World *world = MWBase::Environment::get().getWorld(); if(mDuration) @@ -254,6 +318,12 @@ namespace MWMechanics } } + + + int& cachedCellX = storage.mCellX; + int& cachedCellY = storage.mCellY; + float& cachedCellXposition = storage.mXCell; + float& cachedCellYposition = storage.mYCell; // Initialization to discover & store allowed node points for this actor. if(!mStoredAvailableNodes) { @@ -262,8 +332,8 @@ namespace MWMechanics pathgrid = world->getStore().get().search(*cell); // cache the current cell location - mCellX = cell->mData.mX; - mCellY = cell->mData.mY; + cachedCellX = cell->mData.mX; + cachedCellY = cell->mData.mY; // If there is no path this actor doesn't go anywhere. See: // https://forum.openmw.org/viewtopic.php?t=1556 @@ -277,12 +347,12 @@ namespace MWMechanics // destinations within the allowed set of pathgrid points (nodes). if(mDistance) { - mXCell = 0; - mYCell = 0; + cachedCellXposition = 0; + cachedCellYposition = 0; if(cell->isExterior()) { - mXCell = mCellX * ESM::Land::REAL_SIZE; - mYCell = mCellY * ESM::Land::REAL_SIZE; + cachedCellXposition = cachedCellX * ESM::Land::REAL_SIZE; + cachedCellYposition = cachedCellY * ESM::Land::REAL_SIZE; } // FIXME: There might be a bug here. The allowed node points are @@ -292,8 +362,8 @@ namespace MWMechanics // // convert npcPos to local (i.e. cell) co-ordinates Ogre::Vector3 npcPos(pos.pos); - npcPos[0] = npcPos[0] - mXCell; - npcPos[1] = npcPos[1] - mYCell; + npcPos[0] = npcPos[0] - cachedCellXposition; + npcPos[1] = npcPos[1] - cachedCellYposition; // mAllowedNodes for this actor with pathgrid point indexes based on mDistance // NOTE: mPoints and mAllowedNodes are in local co-ordinates @@ -338,8 +408,8 @@ namespace MWMechanics mHasReturnPosition = false; if (mDistance == 0 && mHasReturnPosition && Ogre::Vector3(pos.pos).squaredDistance(mReturnPosition) > 20*20) { - mChooseAction = false; - mIdleNow = false; + chooseAction = false; + idleNow = false; if (!mPathFinder.isPathConstructed()) { @@ -361,30 +431,32 @@ namespace MWMechanics if(mPathFinder.isPathConstructed()) { - mMoveNow = false; - mWalking = true; + moveNow = false; + walking = true; } } } - if(mChooseAction) + AiWander::GreetingState& greetingState = storage.mSaidGreeting; + short unsigned& playedIdle = storage.mPlayedIdle; + if(chooseAction) { - mPlayedIdle = 0; - getRandomIdle(); // NOTE: sets mPlayedIdle with a random selection + playedIdle = 0; + getRandomIdle(playedIdle); // NOTE: sets mPlayedIdle with a random selection - if(!mPlayedIdle && mDistance) + if(!playedIdle && mDistance) { - mChooseAction = false; - mMoveNow = true; + chooseAction = false; + moveNow = true; } else { // Play idle animation and recreate vanilla (broken?) behavior of resetting start time of AIWander: MWWorld::TimeStamp currentTime = world->getTimeStamp(); mStartTime = currentTime; - playIdle(actor, mPlayedIdle); - mChooseAction = false; - mIdleNow = true; + playIdle(actor, playedIdle); + chooseAction = false; + idleNow = true; // Play idle voiced dialogue entries randomly int hello = cStats.getAiSetting(CreatureStats::AI_Hello).getModified(); @@ -394,82 +466,105 @@ namespace MWMechanics MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); // Don't bother if the player is out of hearing range - if (roll < mChance && Ogre::Vector3(player.getRefData().getPosition().pos).squaredDistance(Ogre::Vector3(pos.pos)) < 1500*1500) + static float fVoiceIdleOdds = MWBase::Environment::get().getWorld()->getStore() + .get().find("fVoiceIdleOdds")->getFloat(); + + // Only say Idle voices when player is in LOS + // A bit counterintuitive, likely vanilla did this to reduce the appearance of + // voices going through walls? + if (roll < fVoiceIdleOdds && Ogre::Vector3(player.getRefData().getPosition().pos).squaredDistance(Ogre::Vector3(pos.pos)) < 1500*1500 + && MWBase::Environment::get().getWorld()->getLOS(player, actor)) MWBase::Environment::get().getDialogueManager()->say(actor, "idle"); } } } // Allow interrupting a walking actor to trigger a greeting - if(mIdleNow || (mWalking && !mObstacleCheck.isNormalState() && mDistance)) + if(idleNow || walking) { // Play a random voice greeting if the player gets too close int hello = cStats.getAiSetting(CreatureStats::AI_Hello).getModified(); float helloDistance = hello; - helloDistance *= mGreetDistanceMultiplier; + static int iGreetDistanceMultiplier =MWBase::Environment::get().getWorld()->getStore() + .get().find("iGreetDistanceMultiplier")->getInt(); + + helloDistance *= iGreetDistanceMultiplier; MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); Ogre::Vector3 playerPos(player.getRefData().getPosition().pos); Ogre::Vector3 actorPos(actor.getRefData().getPosition().pos); float playerDistSqr = playerPos.squaredDistance(actorPos); - - if(playerDistSqr <= helloDistance*helloDistance) + + int& greetingTimer = storage.mGreetingTimer; + if (greetingState == Greet_None) { - if(mWalking) + if ((playerDistSqr <= helloDistance*helloDistance) && + !player.getClass().getCreatureStats(player).isDead() && MWBase::Environment::get().getWorld()->getLOS(player, actor) + && MWBase::Environment::get().getMechanicsManager()->awarenessCheck(player, actor)) + greetingTimer++; + + if (greetingTimer >= GREETING_SHOULD_START) + { + greetingState = Greet_InProgress; + MWBase::Environment::get().getDialogueManager()->say(actor, "hello"); + greetingTimer = 0; + } + } + + if(greetingState == Greet_InProgress) + { + greetingTimer++; + + if(walking) { stopWalking(actor); - mMoveNow = false; - mWalking = false; + moveNow = false; + walking = false; mObstacleCheck.clear(); - mIdleNow = true; - getRandomIdle(); + idleNow = true; + getRandomIdle(playedIdle); } - if(!mRotate) + if(!rotate) { Ogre::Vector3 dir = playerPos - actorPos; - float length = dir.length(); - float faceAngle = Ogre::Radian(Ogre::Math::ACos(dir.y / length) * - ((Ogre::Math::ASin(dir.x / length).valueRadians()>0)?1.0:-1.0)).valueDegrees(); - float actorAngle = actor.getRefData().getBaseNode()->getOrientation().getRoll().valueDegrees(); + Ogre::Radian faceAngle = Ogre::Math::ATan2(dir.x,dir.y); + Ogre::Radian actorAngle = actor.getRefData().getBaseNode()->getOrientation().getRoll(); // an attempt at reducing the turning animation glitch - if(abs(abs(faceAngle) - abs(actorAngle)) >= 5) // TODO: is there a better way? + if( Ogre::Math::Abs( faceAngle - actorAngle ) >= Ogre::Degree(5) ) // TODO: is there a better way? { - mTargetAngle = faceAngle; - mRotate = true; + targetAngle = faceAngle; + rotate = true; } } - } - - if (!mSaidGreeting) - { - // TODO: check if actor is aware / has line of sight - if (playerDistSqr <= helloDistance*helloDistance - // Only play a greeting if the player is not moving - && Ogre::Vector3(player.getClass().getMovementSettings(player).mPosition).squaredLength() == 0) + + if (greetingTimer >= GREETING_SHOULD_END) { - mSaidGreeting = true; - MWBase::Environment::get().getDialogueManager()->say(actor, "hello"); + greetingState = Greet_Done; + greetingTimer = 0; } } - else + + if (greetingState == MWMechanics::AiWander::Greet_Done) { - if (playerDistSqr >= mGreetDistanceReset*mGreetDistanceReset * mGreetDistanceMultiplier*mGreetDistanceMultiplier) - mSaidGreeting = false; + static float fGreetDistanceReset = MWBase::Environment::get().getWorld()->getStore() + .get().find("fGreetDistanceReset")->getFloat(); + + if (playerDistSqr >= fGreetDistanceReset*fGreetDistanceReset) + greetingState = Greet_None; } // Check if idle animation finished - // FIXME: don't stay forever - if(!checkIdle(actor, mPlayedIdle) && playerDistSqr > helloDistance*helloDistance) + if(!checkIdle(actor, playedIdle) && (playerDistSqr > helloDistance*helloDistance || greetingState == MWMechanics::AiWander::Greet_Done)) { - mPlayedIdle = 0; - mIdleNow = false; - mChooseAction = true; + playedIdle = 0; + idleNow = false; + chooseAction = true; } } - if(mMoveNow && mDistance) + if(moveNow && mDistance) { // Construct a new path if there isn't one if(!mPathFinder.isPathConstructed()) @@ -483,8 +578,8 @@ namespace MWMechanics // convert dest to use world co-ordinates ESM::Pathgrid::Point dest; - dest.mX = destNodePos[0] + mXCell; - dest.mY = destNodePos[1] + mYCell; + dest.mX = destNodePos[0] + cachedCellXposition; + dest.mY = destNodePos[1] + cachedCellYposition; dest.mZ = destNodePos[2]; // actor position is already in world co-ordinates @@ -515,8 +610,8 @@ namespace MWMechanics mAllowedNodes.push_back(mCurrentNode); mCurrentNode = temp; - mMoveNow = false; - mWalking = true; + moveNow = false; + walking = true; } // Choose a different node and delete this one from possible nodes because it is uncreachable: else @@ -524,17 +619,6 @@ namespace MWMechanics } } - // Are we there yet? - if(mWalking && - mPathFinder.checkPathCompleted(pos.pos[0], pos.pos[1], pos.pos[2])) - { - stopWalking(actor); - mMoveNow = false; - mWalking = false; - mChooseAction = true; - mHasReturnPosition = false; - } - return false; // AiWander package not yet completed } @@ -626,20 +710,54 @@ namespace MWMechanics } } - void AiWander::getRandomIdle() + void AiWander::getRandomIdle(short unsigned& playedIdle) { unsigned short idleRoll = 0; for(unsigned int counter = 0; counter < mIdle.size(); counter++) { - unsigned short idleChance = mIdleChanceMultiplier * mIdle[counter]; - unsigned short randSelect = (int)(rand() / ((double)RAND_MAX + 1) * int(100 / mIdleChanceMultiplier)); + static float fIdleChanceMultiplier = MWBase::Environment::get().getWorld()->getStore() + .get().find("fIdleChanceMultiplier")->getFloat(); + + unsigned short idleChance = fIdleChanceMultiplier * mIdle[counter]; + unsigned short randSelect = (int)(rand() / ((double)RAND_MAX + 1) * int(100 / fIdleChanceMultiplier)); if(randSelect < idleChance && randSelect > idleRoll) { - mPlayedIdle = counter+2; + playedIdle = counter+2; idleRoll = randSelect; } } } + + void AiWander::writeState(ESM::AiSequence::AiSequence &sequence) const + { + std::auto_ptr wander(new ESM::AiSequence::AiWander()); + wander->mData.mDistance = mDistance; + wander->mData.mDuration = mDuration; + wander->mData.mTimeOfDay = mTimeOfDay; + wander->mStartTime = mStartTime.toEsm(); + assert (mIdle.size() == 8); + for (int i=0; i<8; ++i) + wander->mData.mIdle[i] = mIdle[i]; + wander->mData.mShouldRepeat = mRepeat; + + ESM::AiSequence::AiPackageContainer package; + package.mType = ESM::AiSequence::Ai_Wander; + package.mPackage = wander.release(); + sequence.mPackages.push_back(package); + } + + AiWander::AiWander (const ESM::AiSequence::AiWander* wander) + : mDistance(wander->mData.mDistance) + , mDuration(wander->mData.mDuration) + , mStartTime(MWWorld::TimeStamp(wander->mStartTime)) + , mTimeOfDay(wander->mData.mTimeOfDay) + , mRepeat(wander->mData.mShouldRepeat) + { + for (int i=0; i<8; ++i) + mIdle.push_back(wander->mData.mIdle[i]); + + init(); + } } diff --git a/apps/openmw/mwmechanics/aiwander.hpp b/apps/openmw/mwmechanics/aiwander.hpp index 6481b2a01..0600909ba 100644 --- a/apps/openmw/mwmechanics/aiwander.hpp +++ b/apps/openmw/mwmechanics/aiwander.hpp @@ -12,8 +12,21 @@ #include "../mwworld/timestamp.hpp" -namespace MWMechanics + +#include "aistate.hpp" + +namespace ESM { + namespace AiSequence + { + struct AiWander; + } +} + +namespace MWMechanics +{ + + /// \brief Causes the Actor to wander within a specified range class AiWander : public AiPackage { @@ -24,11 +37,15 @@ namespace MWMechanics \param timeOfDay Start time of the package, if it has a duration. Currently unimplemented \param idle Chances of each idle to play (9 in total) \param repeat Repeat wander or not **/ - AiWander(int distance, int duration, int timeOfDay, const std::vector& idle, bool repeat); + AiWander(int distance, int duration, int timeOfDay, const std::vector& idle, bool repeat); + + AiWander (const ESM::AiSequence::AiWander* wander); + + virtual AiPackage *clone() const; - virtual bool execute (const MWWorld::Ptr& actor,float duration); + virtual bool execute (const MWWorld::Ptr& actor, AiState& state, float duration); virtual int getTypeId() const; @@ -36,46 +53,44 @@ namespace MWMechanics /** In case another AI package moved the actor elsewhere **/ void setReturnPosition (const Ogre::Vector3& position); + virtual void writeState(ESM::AiSequence::AiSequence &sequence) const; + + + enum GreetingState { + Greet_None, + Greet_InProgress, + Greet_Done + }; private: + // NOTE: mDistance and mDuration must be set already + void init(); + void stopWalking(const MWWorld::Ptr& actor); void playIdle(const MWWorld::Ptr& actor, unsigned short idleSelect); bool checkIdle(const MWWorld::Ptr& actor, unsigned short idleSelect); - void getRandomIdle(); + void getRandomIdle(unsigned short& playedIdle); int mDistance; // how far the actor can wander from the spawn point int mDuration; int mTimeOfDay; - std::vector mIdle; + std::vector mIdle; bool mRepeat; - bool mSaidGreeting; - int mGreetDistanceMultiplier; - float mGreetDistanceReset; - float mChance; + bool mHasReturnPosition; // NOTE: Could be removed if mReturnPosition was initialized to actor position, // if we had the actor in the AiWander constructor... Ogre::Vector3 mReturnPosition; - // Cached current cell location - int mCellX; - int mCellY; - // Cell location multiplied by ESM::Land::REAL_SIZE - float mXCell; - float mYCell; - const MWWorld::CellStore* mCell; // for detecting cell change + + // if false triggers calculating allowed nodes based on mDistance bool mStoredAvailableNodes; - // AiWander states - bool mChooseAction; - bool mIdleNow; - bool mMoveNow; - bool mWalking; - float mIdleChanceMultiplier; - unsigned short mPlayedIdle; + + MWWorld::TimeStamp mStartTime; @@ -86,18 +101,16 @@ namespace MWMechanics void trimAllowedNodes(std::vector& nodes, const PathFinder& pathfinder); - PathFinder mPathFinder; +// PathFinder mPathFinder; - ObstacleCheck mObstacleCheck; +// ObstacleCheck mObstacleCheck; float mDoorCheckDuration; int mStuckCount; - // the z rotation angle (degrees) we want to reach - // used every frame when mRotate is true - float mTargetAngle; - bool mRotate; - float mReaction; // update some actions infrequently + }; + + } #endif diff --git a/apps/openmw/mwmechanics/alchemy.cpp b/apps/openmw/mwmechanics/alchemy.cpp index 64b358b96..da2492a77 100644 --- a/apps/openmw/mwmechanics/alchemy.cpp +++ b/apps/openmw/mwmechanics/alchemy.cpp @@ -27,6 +27,11 @@ #include "creaturestats.hpp" #include "npcstats.hpp" +MWMechanics::Alchemy::Alchemy() + : mValue(0) +{ +} + std::set MWMechanics::Alchemy::listEffects() const { std::map effects; @@ -128,7 +133,7 @@ void MWMechanics::Alchemy::updateEffects() std::set effects (listEffects()); // general alchemy factor - float x = getChance(); + float x = getAlchemyFactor(); x *= mTools[ESM::Apparatus::MortarPestle].get()->mBase->mData.mQuality; x *= MWBase::Environment::get().getWorld()->getStore().get().find ("fPotionStrengthMult")->getFloat(); @@ -181,7 +186,13 @@ void MWMechanics::Alchemy::updateEffects() ESM::ENAMstruct effect; effect.mEffectID = iter->mId; - effect.mSkill = effect.mAttribute = iter->mArg; // somewhat hack-ish, but should work + effect.mAttribute = -1; + effect.mSkill = -1; + + if (magicEffect->mData.mFlags & ESM::MagicEffect::TargetSkill) + effect.mSkill = iter->mArg; + else if (magicEffect->mData.mFlags & ESM::MagicEffect::TargetAttribute) + effect.mAttribute = iter->mArg; effect.mRange = 0; effect.mArea = 0; @@ -194,7 +205,7 @@ void MWMechanics::Alchemy::updateEffects() } } -const ESM::Potion *MWMechanics::Alchemy::getRecord() const +const ESM::Potion *MWMechanics::Alchemy::getRecord(const ESM::Potion& toFind) const { const MWWorld::Store &potions = MWBase::Environment::get().getWorld()->getStore().get(); @@ -205,6 +216,18 @@ const ESM::Potion *MWMechanics::Alchemy::getRecord() const if (iter->mEffects.mList.size() != mEffects.size()) continue; + if (iter->mName != toFind.mName + || iter->mScript != toFind.mScript + || iter->mData.mWeight != toFind.mData.mWeight + || iter->mData.mValue != toFind.mData.mValue + || iter->mData.mAutoCalc != toFind.mData.mAutoCalc) + continue; + + // Don't choose an ID that came from the content files, would have unintended side effects + // where alchemy can be used to produce quest-relevant items + if (!potions.isDynamic(iter->mId)) + continue; + bool mismatch = false; for (int i=0; i (iter->mEffects.mList.size()); ++i) @@ -255,37 +278,34 @@ void MWMechanics::Alchemy::removeIngredients() void MWMechanics::Alchemy::addPotion (const std::string& name) { - const ESM::Potion *record = getRecord(); - - if (!record) - { - ESM::Potion newRecord; + ESM::Potion newRecord; - newRecord.mData.mWeight = 0; + newRecord.mData.mWeight = 0; - for (TIngredientsIterator iter (beginIngredients()); iter!=endIngredients(); ++iter) - if (!iter->isEmpty()) - newRecord.mData.mWeight += iter->get()->mBase->mData.mWeight; + for (TIngredientsIterator iter (beginIngredients()); iter!=endIngredients(); ++iter) + if (!iter->isEmpty()) + newRecord.mData.mWeight += iter->get()->mBase->mData.mWeight; - newRecord.mData.mWeight /= countIngredients(); + newRecord.mData.mWeight /= countIngredients(); - newRecord.mData.mValue = mValue; - newRecord.mData.mAutoCalc = 0; + newRecord.mData.mValue = mValue; + newRecord.mData.mAutoCalc = 0; - newRecord.mName = name; + newRecord.mName = name; - int index = static_cast (std::rand()/(static_cast (RAND_MAX)+1)*6); - assert (index>=0 && index<6); + int index = static_cast (std::rand()/(static_cast (RAND_MAX)+1)*6); + assert (index>=0 && index<6); - static const char *name[] = { "standard", "bargain", "cheap", "fresh", "exclusive", "quality" }; + static const char *meshes[] = { "standard", "bargain", "cheap", "fresh", "exclusive", "quality" }; - newRecord.mModel = "m\\misc_potion_" + std::string (name[index]) + "_01.nif"; - newRecord.mIcon = "m\\tx_potion_" + std::string (name[index]) + "_01.dds"; + newRecord.mModel = "m\\misc_potion_" + std::string (meshes[index]) + "_01.nif"; + newRecord.mIcon = "m\\tx_potion_" + std::string (meshes[index]) + "_01.dds"; - newRecord.mEffects.mList = mEffects; + newRecord.mEffects.mList = mEffects; + const ESM::Potion* record = getRecord(newRecord); + if (!record) record = MWBase::Environment::get().getWorld()->createRecord (newRecord); - } mAlchemist.getClass().getContainerStore (mAlchemist).add (record->mId, 1, mAlchemist); } @@ -295,15 +315,15 @@ void MWMechanics::Alchemy::increaseSkill() mAlchemist.getClass().skillUsageSucceeded (mAlchemist, ESM::Skill::Alchemy, 0); } -float MWMechanics::Alchemy::getChance() const +float MWMechanics::Alchemy::getAlchemyFactor() const { const CreatureStats& creatureStats = mAlchemist.getClass().getCreatureStats (mAlchemist); const NpcStats& npcStats = mAlchemist.getClass().getNpcStats (mAlchemist); return (npcStats.getSkill (ESM::Skill::Alchemy).getModified() + - 0.1 * creatureStats.getAttribute (1).getModified() - + 0.1 * creatureStats.getAttribute (7).getModified()); + 0.1 * creatureStats.getAttribute (ESM::Attribute::Intelligence).getModified() + + 0.1 * creatureStats.getAttribute (ESM::Attribute::Luck).getModified()); } int MWMechanics::Alchemy::countIngredients() const @@ -395,7 +415,8 @@ int MWMechanics::Alchemy::addIngredient (const MWWorld::Ptr& ingredient) return -1; for (TIngredientsIterator iter (mIngredients.begin()); iter!=mIngredients.end(); ++iter) - if (!iter->isEmpty() && ingredient.get()==iter->get()) + if (!iter->isEmpty() && Misc::StringUtils::ciEqual(ingredient.getClass().getId(ingredient), + iter->getClass().getId(*iter))) return -1; mIngredients[slot] = ingredient; @@ -424,14 +445,6 @@ MWMechanics::Alchemy::TEffectsIterator MWMechanics::Alchemy::endEffects() const return mEffects.end(); } -std::string MWMechanics::Alchemy::getPotionName() const -{ - if (const ESM::Potion *potion = getRecord()) - return potion->mName; - - return ""; -} - MWMechanics::Alchemy::Result MWMechanics::Alchemy::create (const std::string& name) { if (mTools[ESM::Apparatus::MortarPestle].isEmpty()) @@ -440,13 +453,20 @@ MWMechanics::Alchemy::Result MWMechanics::Alchemy::create (const std::string& na if (countIngredients()<2) return Result_LessThanTwoIngredients; - if (name.empty() && getPotionName().empty()) + if (name.empty()) return Result_NoName; - if (beginEffects()==endEffects()) + if (listEffects().empty()) return Result_NoEffects; - if (getChance() (RAND_MAX)*100) + if (beginEffects() == endEffects()) + { + // all effects were nullified due to insufficient skill + removeIngredients(); + return Result_RandomFailure; + } + + if (getAlchemyFactor() (RAND_MAX)*100) { removeIngredients(); return Result_RandomFailure; @@ -460,3 +480,14 @@ MWMechanics::Alchemy::Result MWMechanics::Alchemy::create (const std::string& na return Result_Success; } + +std::string MWMechanics::Alchemy::suggestPotionName() +{ + std::set effects = listEffects(); + if (effects.empty()) + return ""; + + int effectId = effects.begin()->mId; + return MWBase::Environment::get().getWorld()->getStore().get().find( + ESM::MagicEffect::effectIdToString(effectId))->getString(); +} diff --git a/apps/openmw/mwmechanics/alchemy.hpp b/apps/openmw/mwmechanics/alchemy.hpp index 31cafa4dc..caba26f14 100644 --- a/apps/openmw/mwmechanics/alchemy.hpp +++ b/apps/openmw/mwmechanics/alchemy.hpp @@ -22,6 +22,8 @@ namespace MWMechanics { public: + Alchemy(); + typedef std::vector TToolsContainer; typedef TToolsContainer::const_iterator TToolsIterator; @@ -50,15 +52,13 @@ namespace MWMechanics TEffectsContainer mEffects; int mValue; - std::set listEffects() const; - ///< List all effects shared by at least two ingredients. - void applyTools (int flags, float& value) const; void updateEffects(); - const ESM::Potion *getRecord() const; - ///< Return existing recrod for created potion (may return 0) + const ESM::Potion *getRecord(const ESM::Potion& toFind) const; + ///< Try to find a potion record similar to \a toFind in the record store, or return 0 if not found + /// \note Does not account for record ID, model or icon void removeIngredients(); ///< Remove selected ingredients from alchemist's inventory, cleanup selected ingredients and @@ -70,11 +70,14 @@ namespace MWMechanics void increaseSkill(); ///< Increase alchemist's skill. - float getChance() const; - ///< Return chance of success. + float getAlchemyFactor() const; int countIngredients() const; + TEffectsIterator beginEffects() const; + + TEffectsIterator endEffects() const; + public: void setAlchemist (const MWWorld::Ptr& npc); @@ -94,6 +97,9 @@ namespace MWMechanics void clear(); ///< Remove alchemist, tools and ingredients. + std::set listEffects() const; + ///< List all effects shared by at least two ingredients. + int addIngredient (const MWWorld::Ptr& ingredient); ///< Add ingredient into the next free slot. /// @@ -103,19 +109,13 @@ namespace MWMechanics void removeIngredient (int index); ///< Remove ingredient from slot (calling this function on an empty slot is a no-op). - TEffectsIterator beginEffects() const; - - TEffectsIterator endEffects() const; - - std::string getPotionName() const; - ///< Return the name of the potion that would be created when calling create (if a record for such - /// a potion already exists) or return an empty string. + std::string suggestPotionName (); + ///< Suggest a name for the potion, based on the current effects Result create (const std::string& name); ///< Try to create a potion from the ingredients, place it in the inventory of the alchemist and /// adjust the skills of the alchemist accordingly. - /// \param name must not be an empty string, unless there is already a potion record ( - /// getPotionName() does not return an empty string). + /// \param name must not be an empty string, or Result_NoName is returned }; } diff --git a/apps/openmw/mwmechanics/autocalcspell.cpp b/apps/openmw/mwmechanics/autocalcspell.cpp new file mode 100644 index 000000000..7b8c43a06 --- /dev/null +++ b/apps/openmw/mwmechanics/autocalcspell.cpp @@ -0,0 +1,232 @@ +#include "autocalcspell.hpp" + +#include + +#include "../mwworld/esmstore.hpp" + +#include "../mwbase/world.hpp" +#include "../mwbase/environment.hpp" + + +namespace MWMechanics +{ + + struct SchoolCaps + { + int mCount; + int mLimit; + bool mReachedLimit; + int mMinCost; + std::string mWeakestSpell; + }; + + std::vector autoCalcNpcSpells(const int *actorSkills, const int *actorAttributes, const ESM::Race* race) + { + const MWWorld::Store& gmst = MWBase::Environment::get().getWorld()->getStore().get(); + static const float fNPCbaseMagickaMult = gmst.find("fNPCbaseMagickaMult")->getFloat(); + float baseMagicka = fNPCbaseMagickaMult * actorAttributes[ESM::Attribute::Intelligence]; + + static const std::string schools[] = { + "alteration", "conjuration", "destruction", "illusion", "mysticism", "restoration" + }; + static int iAutoSpellSchoolMax[6]; + static bool init = false; + if (!init) + { + for (int i=0; i<6; ++i) + { + const std::string& gmstName = "iAutoSpell" + schools[i] + "Max"; + iAutoSpellSchoolMax[i] = gmst.find(gmstName)->getInt(); + } + init = true; + } + + std::map schoolCaps; + for (int i=0; i<6; ++i) + { + SchoolCaps caps; + caps.mCount = 0; + caps.mLimit = iAutoSpellSchoolMax[i]; + caps.mReachedLimit = iAutoSpellSchoolMax[i] <= 0; + caps.mMinCost = INT_MAX; + caps.mWeakestSpell.clear(); + schoolCaps[i] = caps; + } + + std::vector selectedSpells; + + const MWWorld::Store &spells = + MWBase::Environment::get().getWorld()->getStore().get(); + + // Note: the algorithm heavily depends on the traversal order of the spells. For vanilla-compatible results the + // Store must preserve the record ordering as it was in the content files. + for (MWWorld::Store::iterator iter = spells.begin(); iter != spells.end(); ++iter) + { + const ESM::Spell* spell = &*iter; + + if (spell->mData.mType != ESM::Spell::ST_Spell) + continue; + if (!(spell->mData.mFlags & ESM::Spell::F_Autocalc)) + continue; + static const int iAutoSpellTimesCanCast = gmst.find("iAutoSpellTimesCanCast")->getInt(); + if (baseMagicka < iAutoSpellTimesCanCast * spell->mData.mCost) + continue; + + if (race && race->mPowers.exists(spell->mId)) + continue; + + if (!attrSkillCheck(spell, actorSkills, actorAttributes)) + continue; + + int school; + float skillTerm; + calcWeakestSchool(spell, actorSkills, school, skillTerm); + assert(school >= 0 && school < 6); + SchoolCaps& cap = schoolCaps[school]; + + if (cap.mReachedLimit && spell->mData.mCost <= cap.mMinCost) + continue; + + static const float fAutoSpellChance = gmst.find("fAutoSpellChance")->getFloat(); + if (calcAutoCastChance(spell, actorSkills, actorAttributes, school) < fAutoSpellChance) + continue; + + selectedSpells.push_back(spell->mId); + + if (cap.mReachedLimit) + { + std::vector::iterator found = std::find(selectedSpells.begin(), selectedSpells.end(), cap.mWeakestSpell); + if (found != selectedSpells.end()) + selectedSpells.erase(found); + + cap.mMinCost = INT_MAX; + for (std::vector::iterator weakIt = selectedSpells.begin(); weakIt != selectedSpells.end(); ++weakIt) + { + const ESM::Spell* testSpell = spells.find(*weakIt); + + //int testSchool; + //float dummySkillTerm; + //calcWeakestSchool(testSpell, actorSkills, testSchool, dummySkillTerm); + + // Note: if there are multiple spells with the same cost, we pick the first one we found. + // So the algorithm depends on the iteration order of the outer loop. + if ( + // There is a huge bug here. It is not checked that weakestSpell is of the correct school. + // As result multiple SchoolCaps could have the same mWeakestSpell. Erasing the weakest spell would then fail if another school + // already erased it, and so the number of spells would often exceed the sum of limits. + // This bug cannot be fixed without significantly changing the results of the spell autocalc, which will not have been playtested. + //testSchool == school && + testSpell->mData.mCost < cap.mMinCost) + { + cap.mMinCost = testSpell->mData.mCost; + cap.mWeakestSpell = testSpell->mId; + } + } + } + else + { + cap.mCount += 1; + if (cap.mCount == cap.mLimit) + cap.mReachedLimit = true; + + if (spell->mData.mCost < cap.mMinCost) + { + cap.mWeakestSpell = spell->mId; + cap.mMinCost = spell->mData.mCost; + } + } + } + + return selectedSpells; + } + + bool attrSkillCheck (const ESM::Spell* spell, const int* actorSkills, const int* actorAttributes) + { + const std::vector& effects = spell->mEffects.mList; + for (std::vector::const_iterator effectIt = effects.begin(); effectIt != effects.end(); ++effectIt) + { + const ESM::MagicEffect* magicEffect = MWBase::Environment::get().getWorld()->getStore().get().find(effectIt->mEffectID); + static const int iAutoSpellAttSkillMin = MWBase::Environment::get().getWorld()->getStore().get().find("iAutoSpellAttSkillMin")->getInt(); + + if ((magicEffect->mData.mFlags & ESM::MagicEffect::TargetSkill)) + { + assert (effectIt->mSkill >= 0 && effectIt->mSkill < ESM::Skill::Length); + if (actorSkills[effectIt->mSkill] < iAutoSpellAttSkillMin) + return false; + } + + if ((magicEffect->mData.mFlags & ESM::MagicEffect::TargetAttribute)) + { + assert (effectIt->mAttribute >= 0 && effectIt->mAttribute < ESM::Attribute::Length); + if (actorAttributes[effectIt->mAttribute] < iAutoSpellAttSkillMin) + return false; + } + } + + return true; + } + + ESM::Skill::SkillEnum mapSchoolToSkill(int school) + { + std::map schoolSkillMap; // maps spell school to skill id + schoolSkillMap[0] = ESM::Skill::Alteration; + schoolSkillMap[1] = ESM::Skill::Conjuration; + schoolSkillMap[3] = ESM::Skill::Illusion; + schoolSkillMap[2] = ESM::Skill::Destruction; + schoolSkillMap[4] = ESM::Skill::Mysticism; + schoolSkillMap[5] = ESM::Skill::Restoration; + assert(schoolSkillMap.find(school) != schoolSkillMap.end()); + return schoolSkillMap[school]; + } + + void calcWeakestSchool (const ESM::Spell* spell, const int* actorSkills, int& effectiveSchool, float& skillTerm) + { + float minChance = FLT_MAX; + + const ESM::EffectList& effects = spell->mEffects; + for (std::vector::const_iterator it = effects.mList.begin(); it != effects.mList.end(); ++it) + { + const ESM::ENAMstruct& effect = *it; + float x = effect.mDuration; + + const ESM::MagicEffect* magicEffect = MWBase::Environment::get().getWorld()->getStore().get().find(effect.mEffectID); + if (!(magicEffect->mData.mFlags & ESM::MagicEffect::UncappedDamage)) + x = std::max(1.f, x); + + x *= 0.1f * magicEffect->mData.mBaseCost; + x *= 0.5f * (effect.mMagnMin + effect.mMagnMax); + x += effect.mArea * 0.05f * magicEffect->mData.mBaseCost; + if (effect.mRange == ESM::RT_Target) + x *= 1.5f; + + static const float fEffectCostMult = MWBase::Environment::get().getWorld()->getStore().get().find("fEffectCostMult")->getFloat(); + x *= fEffectCostMult; + + float s = 2.f * actorSkills[mapSchoolToSkill(magicEffect->mData.mSchool)]; + if (s - x < minChance) + { + minChance = s - x; + effectiveSchool = magicEffect->mData.mSchool; + skillTerm = s; + } + } + } + + float calcAutoCastChance(const ESM::Spell *spell, const int *actorSkills, const int *actorAttributes, int effectiveSchool) + { + if (spell->mData.mType != ESM::Spell::ST_Spell) + return 100.f; + + if (spell->mData.mFlags & ESM::Spell::F_Always) + return 100.f; + + float skillTerm = 0; + if (effectiveSchool != -1) + skillTerm = 2.f * actorSkills[mapSchoolToSkill(effectiveSchool)]; + else + calcWeakestSchool(spell, actorSkills, effectiveSchool, skillTerm); // Note effectiveSchool is unused after this + + float castChance = skillTerm - spell->mData.mCost + 0.2f * actorAttributes[ESM::Attribute::Willpower] + 0.1f * actorAttributes[ESM::Attribute::Luck]; + return castChance; + } +} diff --git a/apps/openmw/mwmechanics/autocalcspell.hpp b/apps/openmw/mwmechanics/autocalcspell.hpp new file mode 100644 index 000000000..1912c75c4 --- /dev/null +++ b/apps/openmw/mwmechanics/autocalcspell.hpp @@ -0,0 +1,31 @@ +#ifndef OPENMW_AUTOCALCSPELL_H +#define OPENMW_AUTOCALCSPELL_H + +#include +#include + +#include +#include +#include + +namespace MWMechanics +{ + +/// Contains algorithm for calculating an NPC's spells based on stats +/// @note We might want to move this code to a component later, so the editor can use it for preview purposes + +std::vector autoCalcNpcSpells(const int* actorSkills, const int* actorAttributes, const ESM::Race* race); + +// Helpers + +bool attrSkillCheck (const ESM::Spell* spell, const int* actorSkills, const int* actorAttributes); + +ESM::Skill::SkillEnum mapSchoolToSkill(int school); + +void calcWeakestSchool(const ESM::Spell* spell, const int* actorSkills, int& effectiveSchool, float& skillTerm); + +float calcAutoCastChance(const ESM::Spell* spell, const int* actorSkills, const int* actorAttributes, int effectiveSchool); + +} + +#endif diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index c9da912dd..72a9bbfde 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -20,6 +20,7 @@ #include "character.hpp" #include +#include #include "movement.hpp" #include "npcstats.hpp" @@ -54,6 +55,43 @@ std::string getBestAttack (const ESM::Weapon* weapon) return "thrust"; } +// Converts a movement Run state to its equivalent Walk state. +MWMechanics::CharacterState runStateToWalkState (MWMechanics::CharacterState state) +{ + using namespace MWMechanics; + CharacterState ret = state; + switch (state) + { + case CharState_RunForward: + ret = CharState_WalkForward; + break; + case CharState_RunBack: + ret = CharState_WalkBack; + break; + case CharState_RunLeft: + ret = CharState_WalkLeft; + break; + case CharState_RunRight: + ret = CharState_WalkRight; + break; + case CharState_SwimRunForward: + ret = CharState_SwimWalkForward; + break; + case CharState_SwimRunBack: + ret = CharState_SwimWalkBack; + break; + case CharState_SwimRunLeft: + ret = CharState_SwimWalkLeft; + break; + case CharState_SwimRunRight: + ret = CharState_SwimWalkRight; + break; + default: + break; + } + return ret; +} + } namespace MWMechanics @@ -158,7 +196,8 @@ void CharacterController::refreshCurrentAnims(CharacterState idle, CharacterStat bool block = mPtr.getClass().getCreatureStats(mPtr).getBlock(); if(mHitState == CharState_None) { - if (mPtr.getClass().getCreatureStats(mPtr).getFatigue().getCurrent() < 0) + if (mPtr.getClass().getCreatureStats(mPtr).getFatigue().getCurrent() < 0 + || mPtr.getClass().getCreatureStats(mPtr).getFatigue().getBase() == 0) { mHitState = CharState_KnockOut; mCurrentHit = "knockout"; @@ -183,6 +222,21 @@ void CharacterController::refreshCurrentAnims(CharacterState idle, CharacterStat mCurrentHit = "shield"; mAnimation->play(mCurrentHit, Priority_Hit, MWRender::Animation::Group_All, true, 1, "block start", "block stop", 0.0f, 0); } + + // Cancel upper body animations + if (mHitState == CharState_KnockDown || mHitState == CharState_KnockOut) + { + if (mUpperBodyState > UpperCharState_WeapEquiped) + { + mAnimation->disable(mCurrentWeapon); + mUpperBodyState = UpperCharState_WeapEquiped; + } + else if (mUpperBodyState > UpperCharState_Nothing && mUpperBodyState < UpperCharState_WeapEquiped) + { + mAnimation->disable(mCurrentWeapon); + mUpperBodyState = UpperCharState_Nothing; + } + } } else if(!mAnimation->isPlaying(mCurrentHit)) { @@ -204,6 +258,8 @@ void CharacterController::refreshCurrentAnims(CharacterState idle, CharacterStat } const WeaponInfo *weap = std::find_if(sWeaponTypeList, sWeaponTypeListEnd, FindWeaponType(mWeaponType)); + if (!mPtr.getClass().hasInventoryStore(mPtr)) + weap = sWeaponTypeListEnd; if(force || idle != mIdleState) { @@ -234,6 +290,8 @@ void CharacterController::refreshCurrentAnims(CharacterState idle, CharacterStat 1.0f, "start", "stop", 0.0f, ~0ul); } + updateIdleStormState(); + if(force && mJumpState != JumpState_None) { std::string jump; @@ -252,7 +310,7 @@ void CharacterController::refreshCurrentAnims(CharacterState idle, CharacterStat } } - if(mJumpState == JumpState_Falling) + if(mJumpState == JumpState_InAir) { int mode = ((jump == mCurrentJump) ? 2 : 1); @@ -296,7 +354,17 @@ void CharacterController::refreshCurrentAnims(CharacterState idle, CharacterStat { std::string::size_type swimpos = movement.find("swim"); if(swimpos == std::string::npos) - movement.clear(); + { + std::string::size_type runpos = movement.find("run"); + if (runpos != std::string::npos) + { + movement.replace(runpos, runpos+3, "walk"); + if (!mAnimation->hasAnimation(movement)) + movement.clear(); + } + else + movement.clear(); + } else { movegroup = MWRender::Animation::Group_LowerBody; @@ -311,27 +379,55 @@ void CharacterController::refreshCurrentAnims(CharacterState idle, CharacterStat * beginning. */ int mode = ((movement == mCurrentMovement) ? 2 : 1); + mMovementAnimationControlled = true; + mAnimation->disable(mCurrentMovement); mCurrentMovement = movement; - mMovementAnimVelocity = 0.0f; if(!mCurrentMovement.empty()) { float vel, speedmult = 1.0f; - bool isrunning = mPtr.getClass().getCreatureStats(mPtr).getStance(MWMechanics::CreatureStats::Stance_Run); + bool isrunning = mPtr.getClass().getCreatureStats(mPtr).getStance(MWMechanics::CreatureStats::Stance_Run) + && !MWBase::Environment::get().getWorld()->isFlying(mPtr); - if(mMovementSpeed > 0.0f && (vel=mAnimation->getVelocity(mCurrentMovement)) > 1.0f) + // For non-flying creatures, MW uses the Walk animation to calculate the animation velocity + // even if we are running. This must be replicated, otherwise the observed speed would differ drastically. + std::string anim = mCurrentMovement; + if (mPtr.getClass().getTypeName() == typeid(ESM::Creature).name() + && !(mPtr.get()->mBase->mFlags & ESM::Creature::Flies)) { - mMovementAnimVelocity = vel; - speedmult = mMovementSpeed / vel; + CharacterState walkState = runStateToWalkState(mMovementState); + const StateInfo *stateinfo = std::find_if(sMovementList, sMovementListEnd, FindCharState(walkState)); + anim = stateinfo->groupname; + + if (mMovementSpeed > 0.0f && (vel=mAnimation->getVelocity(anim)) > 1.0f) + speedmult = mMovementSpeed / vel; + else + // Another bug: when using a fallback animation (e.g. RunForward as fallback to SwimRunForward), + // then the equivalent Walk animation will not use a fallback, and if that animation doesn't exist + // we will play without any scaling. + // Makes the speed attribute of most water creatures totally useless. + // And again, this can not be fixed without patching game data. + speedmult = 1.f; + } + else + { + if(mMovementSpeed > 0.0f && (vel=mAnimation->getVelocity(anim)) > 1.0f) + { + speedmult = mMovementSpeed / vel; + } + else if (mMovementState == CharState_TurnLeft || mMovementState == CharState_TurnRight) + speedmult = 1.f; // adjusted each frame + else if (mMovementSpeed > 0.0f) + { + // The first person anims don't have any velocity to calculate a speed multiplier from. + // We use the third person velocities instead. + // FIXME: should be pulled from the actual animation, but it is not presently loaded. + speedmult = mMovementSpeed / (isrunning ? 222.857f : 154.064f); + mMovementAnimationControlled = false; + } } - else if (mMovementState == CharState_TurnLeft || mMovementState == CharState_TurnRight) - speedmult = 1.f; // TODO: should get a speed mult depending on the current turning speed - else if (mMovementSpeed > 0.0f) - // The first person anims don't have any velocity to calculate a speed multiplier from. - // We use the third person velocities instead. - // FIXME: should be pulled from the actual animation, but it is not presently loaded. - speedmult = mMovementSpeed / (isrunning ? 222.857f : 154.064f); + mAnimation->play(mCurrentMovement, Priority_Movement, movegroup, false, speedmult, ((mode!=2)?"start":"loop start"), "stop", 0.0f, ~0ul); } @@ -428,9 +524,24 @@ void CharacterController::playDeath(float startpoint, CharacterState death) mPtr.getClass().getCreatureStats(mPtr).setDeathAnimation(mDeathState - CharState_Death1); // For dead actors, refreshCurrentAnims is no longer called, so we need to disable the movement state manually. + // Note that these animations wouldn't actually be visible (due to the Death animation's priority being higher). + // However, they could still trigger text keys, such as Hit events, or sounds. mMovementState = CharState_None; mAnimation->disable(mCurrentMovement); mCurrentMovement = ""; + mUpperBodyState = UpperCharState_Nothing; + mAnimation->disable(mCurrentWeapon); + mCurrentWeapon = ""; + mHitState = CharState_None; + mAnimation->disable(mCurrentHit); + mCurrentHit = ""; + mIdleState = CharState_None; + mAnimation->disable(mCurrentIdle); + mCurrentIdle = ""; + mJumpState = JumpState_None; + mAnimation->disable(mCurrentJump); + mCurrentJump = ""; + mMovementAnimationControlled = true; mAnimation->play(mCurrentDeath, Priority_Death, MWRender::Animation::Group_All, false, 1.0f, "start", "stop", startpoint, 0); @@ -438,15 +549,22 @@ void CharacterController::playDeath(float startpoint, CharacterState death) void CharacterController::playRandomDeath(float startpoint) { + if (mPtr == MWBase::Environment::get().getWorld()->getPlayerPtr()) + { + // The first-person animations do not include death, so we need to + // force-switch to third person before playing the death animation. + MWBase::Environment::get().getWorld()->useDeathCamera(); + } + if(MWBase::Environment::get().getWorld()->isSwimming(mPtr) && mAnimation->hasAnimation("swimdeath")) { mDeathState = CharState_SwimDeath; } - else if (mHitState == CharState_KnockDown) + else if (mHitState == CharState_KnockDown && mAnimation->hasAnimation("deathknockdown")) { mDeathState = CharState_DeathKnockDown; } - else if (mHitState == CharState_KnockOut) + else if (mHitState == CharState_KnockOut && mAnimation->hasAnimation("deathknockout")) { mDeathState = CharState_DeathKnockOut; } @@ -465,7 +583,8 @@ CharacterController::CharacterController(const MWWorld::Ptr &ptr, MWRender::Anim , mIdleState(CharState_None) , mMovementState(CharState_None) , mMovementSpeed(0.0f) - , mMovementAnimVelocity(0.0f) + , mHasMovedInXY(false) + , mMovementAnimationControlled(true) , mDeathState(CharState_None) , mHitState(CharState_None) , mUpperBodyState(UpperCharState_Nothing) @@ -474,6 +593,7 @@ CharacterController::CharacterController(const MWWorld::Ptr &ptr, MWRender::Anim , mSkipAnim(false) , mSecondsOfRunning(0) , mSecondsOfSwimming(0) + , mTurnAnimationThreshold(0) { if(!mAnimation) return; @@ -488,11 +608,16 @@ CharacterController::CharacterController(const MWWorld::Ptr &ptr, MWRender::Anim if (cls.hasInventoryStore(mPtr)) { getActiveWeapon(cls.getCreatureStats(mPtr), cls.getInventoryStore(mPtr), &mWeaponType); - if(mWeaponType != WeapType_None) + if (mWeaponType != WeapType_None) { - getWeaponGroup(mWeaponType, mCurrentWeapon); mUpperBodyState = UpperCharState_WeapEquiped; + getWeaponGroup(mWeaponType, mCurrentWeapon); + } + + if(mWeaponType != WeapType_None && mWeaponType != WeapType_Spell && mWeaponType != WeapType_HandToHand) + { mAnimation->showWeapons(true); + mAnimation->setWeaponGroup(mCurrentWeapon); } mAnimation->showCarriedLeft(mWeaponType != WeapType_Spell && mWeaponType != WeapType_HandToHand); } @@ -501,8 +626,8 @@ CharacterController::CharacterController(const MWWorld::Ptr &ptr, MWRender::Anim mIdleState = CharState_Idle; else { - /* FIXME: Get the actual death state used. */ - mDeathState = CharState_Death1; + int deathindex = mPtr.getClass().getCreatureStats(mPtr).getDeathAnimation(); + playDeath(1.0f, CharacterState(CharState_Death1 + deathindex)); } } else @@ -514,13 +639,10 @@ CharacterController::CharacterController(const MWWorld::Ptr &ptr, MWRender::Anim } - if(mDeathState != CharState_None) - { - int deathindex = mPtr.getClass().getCreatureStats(mPtr).getDeathAnimation(); - playDeath(1.0f, CharacterState(CharState_Death1 + deathindex)); - } - else + if(mDeathState == CharState_None) refreshCurrentAnims(mIdleState, mMovementState, true); + + mAnimation->runAnimation(0.f); } CharacterController::~CharacterController() @@ -533,32 +655,150 @@ void CharacterController::updatePtr(const MWWorld::Ptr &ptr) mPtr = ptr; } +void CharacterController::updateIdleStormState() +{ + bool inStormDirection = false; + if (MWBase::Environment::get().getWorld()->isInStorm()) + { + Ogre::Vector3 stormDirection = MWBase::Environment::get().getWorld()->getStormDirection(); + Ogre::Vector3 characterDirection = mPtr.getRefData().getBaseNode()->getOrientation().yAxis(); + inStormDirection = stormDirection.angleBetween(characterDirection) > Ogre::Degree(120); + } + if (inStormDirection && mUpperBodyState == UpperCharState_Nothing && mAnimation->hasAnimation("idlestorm")) + { + float complete = 0; + mAnimation->getInfo("idlestorm", &complete); + + if (complete == 0) + mAnimation->play("idlestorm", Priority_Storm, MWRender::Animation::Group_RightArm, false, + 1.0f, "start", "loop start", 0.0f, 0); + else if (complete == 1) + mAnimation->play("idlestorm", Priority_Storm, MWRender::Animation::Group_RightArm, false, + 1.0f, "loop start", "loop stop", 0.0f, ~0ul); + } + else + { + if (mUpperBodyState == UpperCharState_Nothing) + { + if (mAnimation->isPlaying("idlestorm")) + { + if (mAnimation->getCurrentTime("idlestorm") < mAnimation->getTextKeyTime("idlestorm: loop stop")) + { + mAnimation->play("idlestorm", Priority_Storm, MWRender::Animation::Group_RightArm, true, + 1.0f, "loop stop", "stop", 0.0f, 0); + } + } + } + else + mAnimation->disable("idlestorm"); + } +} + +void CharacterController::castSpell(const std::string &spellid) +{ + static const std::string schools[] = { + "alteration", "conjuration", "destruction", "illusion", "mysticism", "restoration" + }; + + const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); + const ESM::Spell *spell = store.get().find(spellid); + const ESM::ENAMstruct &effectentry = spell->mEffects.mList.at(0); + + const ESM::MagicEffect *effect; + effect = store.get().find(effectentry.mEffectID); + + const ESM::Static* castStatic; + if (!effect->mCasting.empty()) + castStatic = store.get().find (effect->mCasting); + else + castStatic = store.get().find ("VFX_DefaultCast"); + + mAnimation->addEffect("meshes\\" + castStatic->mModel, effect->mIndex); + + MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); + if(!effect->mCastSound.empty()) + sndMgr->playSound3D(mPtr, effect->mCastSound, 1.0f, 1.0f); + else + sndMgr->playSound3D(mPtr, schools[effect->mData.mSchool]+" cast", 1.0f, 1.0f); +} + bool CharacterController::updateCreatureState() { const MWWorld::Class &cls = mPtr.getClass(); CreatureStats &stats = cls.getCreatureStats(mPtr); + WeaponType weapType = WeapType_None; + if(stats.getDrawState() == DrawState_Weapon) + weapType = WeapType_HandToHand; + else if (stats.getDrawState() == DrawState_Spell) + weapType = WeapType_Spell; + + if (weapType != mWeaponType) + { + mWeaponType = weapType; + if (mAnimation->isPlaying(mCurrentWeapon)) + mAnimation->disable(mCurrentWeapon); + } + if(stats.getAttackingOrSpell()) { if(mUpperBodyState == UpperCharState_Nothing && mHitState == CharState_None) { MWBase::Environment::get().getWorld()->breakInvisibility(mPtr); - // These are unique animations and not linked to movement type. Just pick one randomly. - int roll = std::rand()/ (static_cast (RAND_MAX) + 1) * 3; // [0, 2] - if (roll == 0) - mCurrentWeapon = "attack1"; - else if (roll == 1) - mCurrentWeapon = "attack2"; - else - mCurrentWeapon = "attack3"; + std::string startKey = "start"; + std::string stopKey = "stop"; + if (weapType == WeapType_Spell) + { + const std::string spellid = stats.getSpells().getSelectedSpell(); + if (!spellid.empty() && MWBase::Environment::get().getWorld()->startSpellCast(mPtr)) + { + castSpell(spellid); - mAnimation->play(mCurrentWeapon, Priority_Weapon, - MWRender::Animation::Group_All, true, - 1, "start", "stop", - 0.0f, 0); - mUpperBodyState = UpperCharState_StartToMinAttack; + if (!mAnimation->hasAnimation("spellcast")) + MWBase::Environment::get().getWorld()->castSpell(mPtr); // No "release" text key to use, so cast immediately + else + { + const ESM::Spell *spell = MWBase::Environment::get().getWorld()->getStore().get().find(spellid); + const ESM::ENAMstruct &effectentry = spell->mEffects.mList.at(0); + + switch(effectentry.mRange) + { + case 0: mAttackType = "self"; break; + case 1: mAttackType = "touch"; break; + case 2: mAttackType = "target"; break; + } + + startKey = mAttackType + " " + startKey; + stopKey = mAttackType + " " + stopKey; + mCurrentWeapon = "spellcast"; + } + } + else + mCurrentWeapon = ""; + } + if (weapType != WeapType_Spell || !mAnimation->hasAnimation("spellcast")) // Not all creatures have a dedicated spellcast animation + { + int roll = std::rand()/ (static_cast (RAND_MAX) + 1) * 3; // [0, 2] + if (roll == 0) + mCurrentWeapon = "attack1"; + else if (roll == 1) + mCurrentWeapon = "attack2"; + else + mCurrentWeapon = "attack3"; + } + + if (!mCurrentWeapon.empty()) + { + mAnimation->play(mCurrentWeapon, Priority_Weapon, + MWRender::Animation::Group_All, true, + 1, startKey, stopKey, + 0.0f, 0); + mUpperBodyState = UpperCharState_StartToMinAttack; + } } + + stats.setAttackingOrSpell(false); } bool animPlaying = mAnimation->getInfo(mCurrentWeapon); @@ -638,6 +878,7 @@ bool CharacterController::updateWeaponState() { MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); if(cls.getCreatureStats(mPtr).getStance(MWMechanics::CreatureStats::Stance_Run) + && mHasMovedInXY && !MWBase::Environment::get().getWorld()->isSwimming(mPtr) && mWeaponType == WeapType_None) { @@ -695,9 +936,7 @@ bool CharacterController::updateWeaponState() if(!spellid.empty() && MWBase::Environment::get().getWorld()->startSpellCast(mPtr)) { - static const std::string schools[] = { - "alteration", "conjuration", "destruction", "illusion", "mysticism", "restoration" - }; + castSpell(spellid); const ESM::Spell *spell = store.get().find(spellid); const ESM::ENAMstruct &effectentry = spell->mEffects.mList.at(0); @@ -705,19 +944,17 @@ bool CharacterController::updateWeaponState() const ESM::MagicEffect *effect; effect = store.get().find(effectentry.mEffectID); - const ESM::Static* castStatic; - if (!effect->mCasting.empty()) - castStatic = store.get().find (effect->mCasting); + const ESM::Static* castStatic = MWBase::Environment::get().getWorld()->getStore().get().find ("VFX_Hands"); + if (mAnimation->getNode("Left Hand")) + { + mAnimation->addEffect("meshes\\" + castStatic->mModel, -1, false, "Left Hand", effect->mParticle); + mAnimation->addEffect("meshes\\" + castStatic->mModel, -1, false, "Right Hand", effect->mParticle); + } else - castStatic = store.get().find ("VFX_DefaultCast"); - - mAnimation->addEffect("meshes\\" + castStatic->mModel, effect->mIndex); - - castStatic = MWBase::Environment::get().getWorld()->getStore().get().find ("VFX_Hands"); - //mAnimation->addEffect("meshes\\" + castStatic->mModel, -1, false, "Bip01 L Hand", effect->mParticle); - //mAnimation->addEffect("meshes\\" + castStatic->mModel, -1, false, "Bip01 R Hand", effect->mParticle); - mAnimation->addEffect("meshes\\" + castStatic->mModel, -1, false, "Left Hand", effect->mParticle); - mAnimation->addEffect("meshes\\" + castStatic->mModel, -1, false, "Right Hand", effect->mParticle); + { + mAnimation->addEffect("meshes\\" + castStatic->mModel, -1, false, "Bip01 L Hand", effect->mParticle); + mAnimation->addEffect("meshes\\" + castStatic->mModel, -1, false, "Bip01 R Hand", effect->mParticle); + } switch(effectentry.mRange) { @@ -731,12 +968,6 @@ bool CharacterController::updateWeaponState() weapSpeed, mAttackType+" start", mAttackType+" stop", 0.0f, 0); mUpperBodyState = UpperCharState_CastingSpell; - - MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); - if(!effect->mCastSound.empty()) - sndMgr->playSound3D(mPtr, effect->mCastSound, 1.0f, 1.0f); - else - sndMgr->playSound3D(mPtr, schools[effect->mData.mSchool]+" cast", 1.0f, 1.0f); } if (inv.getSelectedEnchantItem() != inv.end()) { @@ -747,6 +978,7 @@ bool CharacterController::updateWeaponState() else if(mWeaponType == WeapType_PickProbe) { MWWorld::Ptr item = *weapon; + // TODO: this will only work for the player, and needs to be fixed if NPCs should ever use lockpicks/probes. MWWorld::Ptr target = MWBase::Environment::get().getWorld()->getFacedObject(); std::string resultMessage, resultSound; @@ -796,6 +1028,15 @@ bool CharacterController::updateWeaponState() animPlaying = mAnimation->getInfo(mCurrentWeapon, &complete); if(mUpperBodyState == UpperCharState_MinAttackToMaxAttack && mHitState != CharState_KnockDown) { + float attackStrength = complete; + if (!mPtr.getClass().isNpc()) + { + // most creatures don't actually have an attack wind-up animation, so use a uniform random value + // (even some creatures that can use weapons don't have a wind-up animation either, e.g. Rieklings) + // Note: vanilla MW uses a random value for *all* non-player actors, but we probably don't need to go that far. + attackStrength = std::min(1.f, 0.1f + std::rand() / float(RAND_MAX)); + } + if(mAttackType != "shoot") { MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); @@ -810,21 +1051,23 @@ bool CharacterController::updateWeaponState() else { std::string sound = "SwishM"; - if(complete < 0.5f) + if(attackStrength < 0.5f) sndMgr->playSound3D(mPtr, sound, 1.0f, 0.8f); //Weak attack - else if(complete < 1.0f) + else if(attackStrength < 1.0f) sndMgr->playSound3D(mPtr, sound, 1.0f, 1.0f); //Medium attack else sndMgr->playSound3D(mPtr, sound, 1.0f, 1.2f); //Strong attack } } - stats.setAttackStrength(complete); + stats.setAttackStrength(attackStrength); mAnimation->disable(mCurrentWeapon); mAnimation->play(mCurrentWeapon, Priority_Weapon, MWRender::Animation::Group_UpperBody, false, weapSpeed, mAttackType+" max attack", mAttackType+" min hit", 1.0f-complete, 0); + + complete = 0.f; mUpperBodyState = UpperCharState_MaxAttackToMinHit; } else if (mHitState == CharState_KnockDown) @@ -968,7 +1211,7 @@ bool CharacterController::updateWeaponState() } //if playing combat animation and lowerbody is not busy switch to whole body animation - if((weaptype != WeapType_None || UpperCharState_UnEquipingWeap) && animPlaying) + if((weaptype != WeapType_None || mUpperBodyState == UpperCharState_UnEquipingWeap) && animPlaying) { if( mMovementState != CharState_None || mJumpState != JumpState_None || @@ -1027,20 +1270,42 @@ void CharacterController::update(float duration) { bool onground = world->isOnGround(mPtr); bool inwater = world->isSwimming(mPtr); - bool isrunning = cls.getCreatureStats(mPtr).getStance(MWMechanics::CreatureStats::Stance_Run); bool sneak = cls.getCreatureStats(mPtr).getStance(MWMechanics::CreatureStats::Stance_Sneak); bool flying = world->isFlying(mPtr); - //Ogre::Vector3 vec = cls.getMovementVector(mPtr); - Ogre::Vector3 vec(cls.getMovementSettings(mPtr).mPosition); - if(vec.z > 0.0f) // to avoid slow-down when jumping + // Can't run while flying (see speed formula in Npc/Creature::getSpeed) + bool isrunning = cls.getCreatureStats(mPtr).getStance(MWMechanics::CreatureStats::Stance_Run) && !flying; + CreatureStats &stats = cls.getCreatureStats(mPtr); + + //Force Jump Logic + + bool isMoving = (std::abs(cls.getMovementSettings(mPtr).mPosition[0]) > .5 || abs(cls.getMovementSettings(mPtr).mPosition[1]) > .5); + if(!inwater && !flying) { - Ogre::Vector2 vecXY = Ogre::Vector2(vec.x, vec.y); - vecXY.normalise(); - vec.x = vecXY.x; - vec.y = vecXY.y; + //Force Jump + if(stats.getMovementFlag(MWMechanics::CreatureStats::Flag_ForceJump)) + { + if(onground) + { + cls.getMovementSettings(mPtr).mPosition[2] = 1; + } + else + cls.getMovementSettings(mPtr).mPosition[2] = 0; + } + //Force Move Jump, only jump if they're otherwise moving + if(stats.getMovementFlag(MWMechanics::CreatureStats::Flag_ForceMoveJump) && isMoving) + { + + if(onground) + { + cls.getMovementSettings(mPtr).mPosition[2] = 1; + } + else + cls.getMovementSettings(mPtr).mPosition[2] = 0; + } } - else - vec.normalise(); + + Ogre::Vector3 vec(cls.getMovementSettings(mPtr).mPosition); + vec.normalise(); if(mHitState != CharState_None && mJumpState == JumpState_None) vec = Ogre::Vector3(0.0f); @@ -1055,11 +1320,12 @@ void CharacterController::update(float duration) CharacterState idlestate = CharState_SpecialIdle; bool forcestateupdate = false; - isrunning = isrunning && std::abs(vec[0])+std::abs(vec[1]) > 0.0f; + mHasMovedInXY = std::abs(vec[0])+std::abs(vec[1]) > 0.0f; + isrunning = isrunning && mHasMovedInXY; // advance athletics - if(std::abs(vec[0])+std::abs(vec[1]) > 0.0f && mPtr.getRefData().getHandle() == "player") + if(mHasMovedInXY && mPtr.getRefData().getHandle() == "player") { if(inwater) { @@ -1122,6 +1388,7 @@ void CharacterController::update(float duration) if (inwater || flying) cls.getCreatureStats(mPtr).land(); + bool inJump = true; if(!onground && !flying && !inwater) { // In the air (either getting up ā€”ascending part of jumpā€” or falling). @@ -1132,32 +1399,28 @@ void CharacterController::update(float duration) cls.getCreatureStats(mPtr).land(); } - forcestateupdate = (mJumpState != JumpState_Falling); - mJumpState = JumpState_Falling; + forcestateupdate = (mJumpState != JumpState_InAir); + mJumpState = JumpState_InAir; // This is a guess. All that seems to be known is that "While the player is in the - // air, fJumpMoveBase and fJumpMoveMult governs air control." Assuming Acrobatics - // plays a role, this makes the most sense. - float mult = 0.0f; - if(cls.isNpc()) - { - const NpcStats &stats = cls.getNpcStats(mPtr); - static const float fJumpMoveBase = gmst.find("fJumpMoveBase")->getFloat(); - static const float fJumpMoveMult = gmst.find("fJumpMoveMult")->getFloat(); + // air, fJumpMoveBase and fJumpMoveMult governs air control". What does fJumpMoveMult do? + static const float fJumpMoveBase = gmst.find("fJumpMoveBase")->getFloat(); - mult = fJumpMoveBase + - (stats.getSkill(ESM::Skill::Acrobatics).getModified()/100.0f * - fJumpMoveMult); - } - - vec.x *= mult; - vec.y *= mult; + vec.x *= fJumpMoveBase; + vec.y *= fJumpMoveBase; vec.z = 0.0f; } else if(vec.z > 0.0f && mJumpState == JumpState_None) { // Started a jump. - vec.z = cls.getJump(mPtr); + float z = cls.getJump(mPtr); + if(vec.x == 0 && vec.y == 0) + vec = Ogre::Vector3(0.0f, 0.0f, z); + else + { + Ogre::Vector3 lat = Ogre::Vector3(vec.x, vec.y, 0.0f).normalisedCopy(); + vec = Ogre::Vector3(lat.x, lat.y, 1.0f) * z * 0.707f; + } // advance acrobatics if (mPtr.getRefData().getHandle() == "player") @@ -1167,13 +1430,15 @@ void CharacterController::update(float duration) const MWWorld::Store &gmst = world->getStore().get(); const float fatigueJumpBase = gmst.find("fFatigueJumpBase")->getFloat(); const float fatigueJumpMult = gmst.find("fFatigueJumpMult")->getFloat(); - const float normalizedEncumbrance = cls.getEncumbrance(mPtr) / cls.getCapacity(mPtr); + float normalizedEncumbrance = mPtr.getClass().getNormalizedEncumbrance(mPtr); + if (normalizedEncumbrance > 1) + normalizedEncumbrance = 1; const int fatigueDecrease = fatigueJumpBase + (1 - normalizedEncumbrance) * fatigueJumpMult; DynamicStat fatigue = cls.getCreatureStats(mPtr).getFatigue(); fatigue.setCurrent(fatigue.getCurrent() - fatigueDecrease); cls.getCreatureStats(mPtr).setFatigue(fatigue); } - else if(mJumpState == JumpState_Falling) + else if(mJumpState == JumpState_InAir) { forcestateupdate = true; mJumpState = JumpState_Landing; @@ -1207,10 +1472,11 @@ void CharacterController::update(float duration) } else { - if(!(vec.z > 0.0f)) - mJumpState = JumpState_None; + mJumpState = JumpState_None; vec.z = 0.0f; + inJump = false; + if(std::abs(vec.x/2.0f) > std::abs(vec.y)) { if(vec.x > 0.0f) @@ -1245,6 +1511,15 @@ void CharacterController::update(float duration) } } + mTurnAnimationThreshold -= duration; + if (movestate == CharState_TurnRight || movestate == CharState_TurnLeft) + mTurnAnimationThreshold = 0.05; + else if (movestate == CharState_None && (mMovementState == CharState_TurnRight || mMovementState == CharState_TurnLeft) + && mTurnAnimationThreshold > 0) + { + movestate = mMovementState; + } + if (onground) cls.getCreatureStats(mPtr).land(); @@ -1272,6 +1547,14 @@ void CharacterController::update(float duration) forcestateupdate = updateCreatureState() || forcestateupdate; refreshCurrentAnims(idlestate, movestate, forcestateupdate); + if (inJump) + mMovementAnimationControlled = false; + + if (mMovementState == CharState_TurnLeft || mMovementState == CharState_TurnRight) + { + if (duration > 0) + mAnimation->adjustSpeedMult(mCurrentMovement, std::min(1.5f, std::abs(rot.z) / duration / Ogre::Math::PI)); + } if (!mSkipAnim) { @@ -1283,12 +1566,17 @@ void CharacterController::update(float duration) else //avoid z-rotating for knockdown world->rotateObject(mPtr, rot.x, rot.y, 0.0f, true); - if (mMovementAnimVelocity == 0) + if (!mMovementAnimationControlled) world->queueMovement(mPtr, vec); } + else + // We must always queue movement, even if there is none, to apply gravity. + world->queueMovement(mPtr, Ogre::Vector3(0.0f)); movement = vec; - cls.getMovementSettings(mPtr).mPosition[0] = cls.getMovementSettings(mPtr).mPosition[1] = cls.getMovementSettings(mPtr).mPosition[2] = 0; + cls.getMovementSettings(mPtr).mPosition[0] = cls.getMovementSettings(mPtr).mPosition[1] = 0; + // Can't reset jump state (mPosition[2]) here; we don't know for sure whether the PhysicSystem will actually handle it in this frame + // due to the fixed minimum timestep used for the physics update. It will be reset in PhysicSystem::move once the jump is handled. } else if(cls.getCreatureStats(mPtr).isDead()) { @@ -1324,10 +1612,14 @@ void CharacterController::update(float duration) } // Update movement - if(mMovementAnimVelocity > 0) + if(mMovementAnimationControlled && mPtr.getClass().isActor()) world->queueMovement(mPtr, moved); } + else + mAnimation->updateEffects(duration); mSkipAnim = false; + + mAnimation->enableHeadAnimation(cls.isActor() && !cls.getCreatureStats(mPtr).isDead()); } @@ -1415,6 +1707,10 @@ bool CharacterController::kill() mIdleState = CharState_None; mCurrentIdle.clear(); + // Play Death Music if it was the player dying + if(mPtr.getRefData().getHandle()=="player") + MWBase::Environment::get().getSoundManager()->streamMusic("Special/MW_Death.mp3"); + return true; } @@ -1441,7 +1737,7 @@ void CharacterController::updateContinuousVfx() for (std::vector::iterator it = effects.begin(); it != effects.end(); ++it) { if (mPtr.getClass().getCreatureStats(mPtr).isDead() - || mPtr.getClass().getCreatureStats(mPtr).getMagicEffects().get(MWMechanics::EffectKey(*it)).mMagnitude <= 0) + || mPtr.getClass().getCreatureStats(mPtr).getMagicEffects().get(MWMechanics::EffectKey(*it)).getMagnitude() <= 0) mAnimation->removeEffect(*it); } } @@ -1451,20 +1747,23 @@ void CharacterController::updateVisibility() if (!mPtr.getClass().isActor()) return; float alpha = 1.f; - if (mPtr.getClass().getCreatureStats(mPtr).getMagicEffects().get(ESM::MagicEffect::Invisibility).mMagnitude) + if (mPtr.getClass().getCreatureStats(mPtr).getMagicEffects().get(ESM::MagicEffect::Invisibility).getMagnitude()) { if (mPtr.getRefData().getHandle() == "player") alpha = 0.4f; else alpha = 0.f; } - float chameleon = mPtr.getClass().getCreatureStats(mPtr).getMagicEffects().get(ESM::MagicEffect::Chameleon).mMagnitude; + float chameleon = mPtr.getClass().getCreatureStats(mPtr).getMagicEffects().get(ESM::MagicEffect::Chameleon).getMagnitude(); if (chameleon) { alpha *= std::max(0.2f, (100.f - chameleon)/100.f); } mAnimation->setAlpha(alpha); + + float light = mPtr.getClass().getCreatureStats(mPtr).getMagicEffects().get(ESM::MagicEffect::Light).getMagnitude(); + mAnimation->setLightEffect(light); } void CharacterController::determineAttackType() @@ -1473,9 +1772,9 @@ void CharacterController::determineAttackType() if(mPtr.getClass().hasInventoryStore(mPtr)) { - if (move[1]) // forward-backward + if (move[1] && !move[0]) // forward-backward mAttackType = "thrust"; - else if (move[0]) //sideway + else if (move[0] && !move[1]) //sideway mAttackType = "slash"; else mAttackType = "chop"; diff --git a/apps/openmw/mwmechanics/character.hpp b/apps/openmw/mwmechanics/character.hpp index 5cefe13bc..2a14c53b9 100644 --- a/apps/openmw/mwmechanics/character.hpp +++ b/apps/openmw/mwmechanics/character.hpp @@ -6,6 +6,7 @@ #include #include "../mwworld/ptr.hpp" +#include "aistate.hpp" namespace MWWorld { @@ -32,6 +33,7 @@ enum Priority { Priority_Weapon, Priority_Knockdown, Priority_Torch, + Priority_Storm, Priority_Death, @@ -129,7 +131,7 @@ enum UpperBodyCharacterState { enum JumpingState { JumpState_None, - JumpState_Falling, + JumpState_InAir, JumpState_Landing }; @@ -137,6 +139,9 @@ class CharacterController { MWWorld::Ptr mPtr; MWRender::Animation *mAnimation; + + // + AiState mAiState; typedef std::deque > AnimationQueue; AnimationQueue mAnimQueue; @@ -147,7 +152,8 @@ class CharacterController CharacterState mMovementState; std::string mCurrentMovement; float mMovementSpeed; - float mMovementAnimVelocity; + bool mHasMovedInXY; + bool mMovementAnimationControlled; CharacterState mDeathState; std::string mCurrentDeath; @@ -169,6 +175,8 @@ class CharacterController float mSecondsOfSwimming; float mSecondsOfRunning; + float mTurnAnimationThreshold; // how long to continue playing turning animation after actor stopped turning + std::string mAttackType; // slash, chop or thrust void determineAttackType(); @@ -178,6 +186,9 @@ class CharacterController bool updateWeaponState(); bool updateCreatureState(); + void updateIdleStormState(); + + void castSpell(const std::string& spellid); void updateVisibility(); @@ -211,6 +222,8 @@ public: { return mDeathState != CharState_None; } void forceStateUpdate(); + + AiState& getAiState() { return mAiState; } }; void getWeaponGroup(WeaponType weaptype, std::string &group); diff --git a/apps/openmw/mwmechanics/combat.cpp b/apps/openmw/mwmechanics/combat.cpp index 69c3c08f7..9225a5799 100644 --- a/apps/openmw/mwmechanics/combat.cpp +++ b/apps/openmw/mwmechanics/combat.cpp @@ -10,6 +10,7 @@ #include "../mwmechanics/npcstats.hpp" #include "../mwmechanics/movement.hpp" #include "../mwmechanics/spellcasting.hpp" +#include "../mwmechanics/difficultyscaling.hpp" #include "../mwworld/class.hpp" #include "../mwworld/inventorystore.hpp" @@ -54,11 +55,24 @@ namespace MWMechanics if (!blocker.getClass().hasInventoryStore(blocker)) return false; - if (blocker.getClass().getCreatureStats(blocker).getKnockedDown() - || blocker.getClass().getCreatureStats(blocker).getHitRecovery()) + MWMechanics::CreatureStats& blockerStats = blocker.getClass().getCreatureStats(blocker); + + if (blockerStats.getKnockedDown() // Used for both knockout or knockdown + || blockerStats.getHitRecovery() + || blockerStats.getMagicEffects().get(ESM::MagicEffect::Paralyze).getMagnitude() > 0) + return false; + + // Don't block when in spellcasting state (shield is equipped, but not visible) + if (blockerStats.getDrawState() == DrawState_Spell) return false; MWWorld::InventoryStore& inv = blocker.getClass().getInventoryStore(blocker); + + // Don't block when in hand-to-hand combat (shield is equipped, but not visible) + if (blockerStats.getDrawState() == DrawState_Weapon && + inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight) == inv.end()) + return false; + MWWorld::ContainerStoreIterator shield = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); if (shield == inv.end() || shield->getTypeName() != typeid(ESM::Armor).name()) return false; @@ -72,10 +86,6 @@ namespace MWMechanics if (angle.valueDegrees() > gmst.find("fCombatBlockRightAngle")->getFloat()) return false; - MWMechanics::CreatureStats& blockerStats = blocker.getClass().getCreatureStats(blocker); - if (blockerStats.getDrawState() == DrawState_Spell) - return false; - MWMechanics::CreatureStats& attackerStats = attacker.getClass().getCreatureStats(attacker); float blockTerm = blocker.getClass().getSkill(blocker, ESM::Skill::Block) + 0.2 * blockerStats.getAttribute(ESM::Attribute::Agility).getModified() @@ -114,7 +124,7 @@ namespace MWMechanics const float fFatigueBlockMult = gmst.find("fFatigueBlockMult")->getFloat(); const float fWeaponFatigueBlockMult = gmst.find("fWeaponFatigueBlockMult")->getFloat(); MWMechanics::DynamicStat fatigue = blockerStats.getFatigue(); - float normalizedEncumbrance = blocker.getClass().getEncumbrance(blocker) / blocker.getClass().getCapacity(blocker); + float normalizedEncumbrance = blocker.getClass().getNormalizedEncumbrance(blocker); normalizedEncumbrance = std::min(1.f, normalizedEncumbrance); float fatigueLoss = fFatigueBlockBase + normalizedEncumbrance * fFatigueBlockMult; fatigueLoss += weapon.getClass().getWeight(weapon) * attackerStats.getAttackStrength() * fWeaponFatigueBlockMult; @@ -123,7 +133,7 @@ namespace MWMechanics blockerStats.setBlock(true); - if (blocker.getClass().isNpc()) + if (blocker.getCellRef().getRefId() == "player") blocker.getClass().skillUsageSucceeded(blocker, ESM::Skill::Block, 0); return true; @@ -134,14 +144,10 @@ namespace MWMechanics void resistNormalWeapon(const MWWorld::Ptr &actor, const MWWorld::Ptr& attacker, const MWWorld::Ptr &weapon, float &damage) { MWMechanics::CreatureStats& stats = actor.getClass().getCreatureStats(actor); - float resistance = std::min(100.f, stats.getMagicEffects().get(ESM::MagicEffect::ResistNormalWeapons).mMagnitude - - stats.getMagicEffects().get(ESM::MagicEffect::WeaknessToNormalWeapons).mMagnitude); + float resistance = std::min(100.f, stats.getMagicEffects().get(ESM::MagicEffect::ResistNormalWeapons).getMagnitude() + - stats.getMagicEffects().get(ESM::MagicEffect::WeaknessToNormalWeapons).getMagnitude()); - float multiplier = 0; - if (resistance >= 0) - multiplier = 1 - resistance / 100.f; - else - multiplier = -(resistance-100) / 100.f; + float multiplier = 1.f - resistance / 100.f; if (!(weapon.get()->mBase->mData.mFlags & ESM::Weapon::Silver || weapon.get()->mBase->mData.mFlags & ESM::Weapon::Magical)) @@ -163,12 +169,12 @@ namespace MWMechanics MWMechanics::CreatureStats& attackerStats = attacker.getClass().getCreatureStats(attacker); - const MWWorld::Class &othercls = victim.getClass(); - if(!othercls.isActor()) // Can't hit non-actors - return; - MWMechanics::CreatureStats &otherstats = victim.getClass().getCreatureStats(victim); - if(otherstats.isDead()) // Can't hit dead actors + if(victim.isEmpty() || !victim.getClass().isActor() || victim.getClass().getCreatureStats(victim).isDead()) + // Can't hit non-actors or dead actors + { + reduceWeaponCondition(0.f, false, weapon, attacker); return; + } if(attacker.getRefData().getHandle() == "player") MWBase::Environment::get().getWindowManager()->setEnemy(victim); @@ -183,16 +189,15 @@ namespace MWMechanics if((::rand()/(RAND_MAX+1.0)) > getHitChance(attacker, victim, skillValue)/100.0f) { victim.getClass().onHit(victim, 0.0f, false, projectile, attacker, false); + MWMechanics::reduceWeaponCondition(0.f, false, weapon, attacker); return; } - float damage = 0.0f; - float fDamageStrengthBase = gmst.find("fDamageStrengthBase")->getFloat(); float fDamageStrengthMult = gmst.find("fDamageStrengthMult")->getFloat(); const unsigned char* attack = weapon.get()->mBase->mData.mChop; - damage = attack[0] + ((attack[1]-attack[0])*attackerStats.getAttackStrength()); // Bow/crossbow damage + float damage = attack[0] + ((attack[1]-attack[0])*attackerStats.getAttackStrength()); // Bow/crossbow damage if (weapon != projectile) { // Arrow/bolt damage @@ -203,16 +208,12 @@ namespace MWMechanics damage *= fDamageStrengthBase + (attackerStats.getAttribute(ESM::Attribute::Strength).getModified() * fDamageStrengthMult * 0.1); + adjustWeaponDamage(damage, weapon); + reduceWeaponCondition(damage, true, weapon, attacker); + if(attacker.getRefData().getHandle() == "player") attacker.getClass().skillUsageSucceeded(attacker, weapskill, 0); - bool detected = MWBase::Environment::get().getMechanicsManager()->awarenessCheck(attacker, victim); - if(!detected) - { - damage *= gmst.find("fCombatCriticalStrikeMult")->getFloat(); - MWBase::Environment::get().getWindowManager()->messageBox("#{sTargetCriticalStrike}"); - MWBase::Environment::get().getSoundManager()->playSound3D(victim, "critical damage", 1.0f, 1.0f); - } if (victim.getClass().getCreatureStats(victim).getKnockedDown()) damage *= gmst.find("fCombatKODamageMult")->getFloat(); @@ -224,9 +225,13 @@ namespace MWMechanics if (damage > 0) MWBase::Environment::get().getWorld()->spawnBloodEffect(victim, hitPosition); - float fProjectileThrownStoreChance = gmst.find("fProjectileThrownStoreChance")->getFloat(); - if ((::rand()/(RAND_MAX+1.0)) < fProjectileThrownStoreChance/100.f) - victim.getClass().getContainerStore(victim).add(projectile, 1, victim); + // Arrows shot at enemies have a chance to turn up in their inventory + if (victim != MWBase::Environment::get().getWorld()->getPlayerPtr()) + { + float fProjectileThrownStoreChance = gmst.find("fProjectileThrownStoreChance")->getFloat(); + if ((::rand()/(RAND_MAX+1.0)) < fProjectileThrownStoreChance/100.f) + victim.getClass().getContainerStore(victim).add(projectile, 1, victim); + } victim.getClass().onHit(victim, damage, true, projectile, attacker, true); } @@ -239,10 +244,94 @@ namespace MWMechanics (stats.getAttribute(ESM::Attribute::Agility).getModified() / 5.0f) + (stats.getAttribute(ESM::Attribute::Luck).getModified() / 10.0f); hitchance *= stats.getFatigueTerm(); - hitchance += mageffects.get(ESM::MagicEffect::FortifyAttack).mMagnitude - - mageffects.get(ESM::MagicEffect::Blind).mMagnitude; + hitchance += mageffects.get(ESM::MagicEffect::FortifyAttack).getMagnitude() - + mageffects.get(ESM::MagicEffect::Blind).getMagnitude(); hitchance -= victim.getClass().getCreatureStats(victim).getEvasion(); return hitchance; } + void applyElementalShields(const MWWorld::Ptr &attacker, const MWWorld::Ptr &victim) + { + for (int i=0; i<3; ++i) + { + float magnitude = victim.getClass().getCreatureStats(victim).getMagicEffects().get(ESM::MagicEffect::FireShield+i).getMagnitude(); + + if (!magnitude) + continue; + + CreatureStats& attackerStats = attacker.getClass().getCreatureStats(attacker); + float saveTerm = attacker.getClass().getSkill(attacker, ESM::Skill::Destruction) + + 0.2f * attackerStats.getAttribute(ESM::Attribute::Willpower).getModified() + + 0.1f * attackerStats.getAttribute(ESM::Attribute::Luck).getModified(); + + int fatigueMax = attackerStats.getFatigue().getModified(); + int fatigueCurrent = attackerStats.getFatigue().getCurrent(); + + float normalisedFatigue = fatigueMax==0 ? 1 : std::max (0.0f, static_cast (fatigueCurrent)/fatigueMax); + + saveTerm *= 1.25f * normalisedFatigue; + + float roll = std::rand()/ (static_cast (RAND_MAX) + 1) * 100; // [0, 99] + float x = std::max(0.f, saveTerm - roll); + + int element = ESM::MagicEffect::FireDamage; + if (i == 1) + element = ESM::MagicEffect::ShockDamage; + if (i == 2) + element = ESM::MagicEffect::FrostDamage; + + float elementResistance = MWMechanics::getEffectResistanceAttribute(element, &attackerStats.getMagicEffects()); + + x = std::min(100.f, x + elementResistance); + + static const float fElementalShieldMult = MWBase::Environment::get().getWorld()->getStore().get().find("fElementalShieldMult")->getFloat(); + x = fElementalShieldMult * magnitude * (1.f - 0.01f * x); + + // Note swapped victim and attacker, since the attacker takes the damage here. + x = scaleDamage(x, victim, attacker); + + MWMechanics::DynamicStat health = attackerStats.getHealth(); + health.setCurrent(health.getCurrent() - x); + attackerStats.setHealth(health); + } + } + + void reduceWeaponCondition(float damage, bool hit, MWWorld::Ptr &weapon, const MWWorld::Ptr &attacker) + { + if (weapon.isEmpty()) + return; + + if (!hit) + damage = 0.f; + + const bool weaphashealth = weapon.getClass().hasItemHealth(weapon); + if(weaphashealth) + { + int weaphealth = weapon.getClass().getItemHealth(weapon); + + const float fWeaponDamageMult = MWBase::Environment::get().getWorld()->getStore().get().find("fWeaponDamageMult")->getFloat(); + float x = std::max(1.f, fWeaponDamageMult * damage); + + weaphealth -= std::min(int(x), weaphealth); + weapon.getCellRef().setCharge(weaphealth); + + // Weapon broken? unequip it + if (weaphealth == 0) + weapon = *attacker.getClass().getInventoryStore(attacker).unequipItem(weapon, attacker); + } + } + + void adjustWeaponDamage(float &damage, const MWWorld::Ptr &weapon) + { + if (weapon.isEmpty()) + return; + + const bool weaphashealth = weapon.getClass().hasItemHealth(weapon); + if(weaphashealth) + { + int weaphealth = weapon.getClass().getItemHealth(weapon); + int weapmaxhealth = weapon.getClass().getItemMaxHealth(weapon); + damage *= (float(weaphealth) / weapmaxhealth); + } + } } diff --git a/apps/openmw/mwmechanics/combat.hpp b/apps/openmw/mwmechanics/combat.hpp index bc58227bf..0d3009510 100644 --- a/apps/openmw/mwmechanics/combat.hpp +++ b/apps/openmw/mwmechanics/combat.hpp @@ -13,12 +13,25 @@ bool blockMeleeAttack (const MWWorld::Ptr& attacker, const MWWorld::Ptr& blocker void resistNormalWeapon (const MWWorld::Ptr& actor, const MWWorld::Ptr& attacker, const MWWorld::Ptr& weapon, float& damage); /// @note for a thrown weapon, \a weapon == \a projectile, for bows/crossbows, \a projectile is the arrow/bolt +/// @note \a victim may be empty (e.g. for a hit on terrain), a non-actor (environment objects) or an actor void projectileHit (const MWWorld::Ptr& attacker, const MWWorld::Ptr& victim, MWWorld::Ptr weapon, const MWWorld::Ptr& projectile, const Ogre::Vector3& hitPosition); /// Get the chance (in percent) for \a attacker to successfully hit \a victim with a given weapon skill value float getHitChance (const MWWorld::Ptr& attacker, const MWWorld::Ptr& victim, int skillValue); +/// Applies damage to attacker based on the victim's elemental shields. +void applyElementalShields(const MWWorld::Ptr& attacker, const MWWorld::Ptr& victim); + +/// @param damage Unmitigated weapon damage of the attack +/// @param hit Was the attack successful? +/// @param weapon The weapon used. +/// @note if the weapon is unequipped as result of condition damage, a new Ptr will be assigned to \a weapon. +void reduceWeaponCondition (float damage, bool hit, MWWorld::Ptr& weapon, const MWWorld::Ptr& attacker); + +/// Adjust weapon damage based on its condition. A used weapon will be less effective. +void adjustWeaponDamage (float& damage, const MWWorld::Ptr& weapon); + } #endif diff --git a/apps/openmw/mwmechanics/creaturestats.cpp b/apps/openmw/mwmechanics/creaturestats.cpp index 7fd26c25c..c4d316ad6 100644 --- a/apps/openmw/mwmechanics/creaturestats.cpp +++ b/apps/openmw/mwmechanics/creaturestats.cpp @@ -8,15 +8,16 @@ #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" +#include "../mwbase/mechanicsmanager.hpp" namespace MWMechanics { int CreatureStats::sActorId = 0; CreatureStats::CreatureStats() - : mLevel (0), mDead (false), mDied (false), mFriendlyHits (0), + : mLevel (0), mDead (false), mDied (false), mMurdered(false), mFriendlyHits (0), mTalkedTo (false), mAlarmed (false), - mAttacked (false), mHostile (false), + mAttacked (false), mAttackingOrSpell(false), mIsWerewolf(false), mFallHeight(0), mRecalcDynamicStats(false), mKnockdown(false), mKnockdownOneFrame(false), @@ -119,11 +120,6 @@ namespace MWMechanics return mSpells; } - void CreatureStats::setSpells(const Spells &spells) - { - mSpells = spells; - } - ActiveSpells &CreatureStats::getActiveSpells() { return mActiveSpells; @@ -187,10 +183,10 @@ namespace MWMechanics if (index==0 && mDynamic[index].getCurrent()<1) { - if (!mDead) - mDied = true; - mDead = true; + + if (MWBase::Environment::get().getWorld()->getGodModeState()) + MWBase::Environment::get().getMechanicsManager()->keepPlayerAlive(); } } @@ -199,18 +195,13 @@ namespace MWMechanics mLevel = level; } - void CreatureStats::setActiveSpells(const ActiveSpells &active) + void CreatureStats::modifyMagicEffects(const MagicEffects &effects) { - mActiveSpells = active; - } - - void CreatureStats::setMagicEffects(const MagicEffects &effects) - { - if (effects.get(ESM::MagicEffect::FortifyMaximumMagicka).mMagnitude - != mMagicEffects.get(ESM::MagicEffect::FortifyMaximumMagicka).mMagnitude) + if (effects.get(ESM::MagicEffect::FortifyMaximumMagicka).getModifier() + != mMagicEffects.get(ESM::MagicEffect::FortifyMaximumMagicka).getModifier()) mRecalcDynamicStats = true; - mMagicEffects = effects; + mMagicEffects.setModifiers(effects); } void CreatureStats::setAttackingOrSpell(bool attackingOrSpell) @@ -235,6 +226,11 @@ namespace MWMechanics return mDead; } + void CreatureStats::notifyDied() + { + mDied = true; + } + bool CreatureStats::hasDied() const { return mDied; @@ -245,15 +241,26 @@ namespace MWMechanics mDied = false; } + bool CreatureStats::hasBeenMurdered() const + { + return mMurdered; + } + + void CreatureStats::notifyMurder() + { + mMurdered = true; + } + + void CreatureStats::clearHasBeenMurdered() + { + mMurdered = false; + } + void CreatureStats::resurrect() { if (mDead) { - if (mDynamic[0].getCurrent()<1) - { - mDynamic[0].setModified(mDynamic[0].getModified(), 1); - mDynamic[0].setCurrent(1); - } + mDynamic[0].setCurrent(mDynamic[0].getModified()); if (mDynamic[0].getCurrent()>=1) mDead = false; } @@ -309,16 +316,6 @@ namespace MWMechanics mAttacked = attacked; } - bool CreatureStats::isHostile() const - { - return mHostile; - } - - void CreatureStats::setHostile (bool hostile) - { - mHostile = hostile; - } - bool CreatureStats::getCreatureTargetted() const { MWWorld::Ptr targetPtr; @@ -334,7 +331,7 @@ namespace MWMechanics float evasion = (getAttribute(ESM::Attribute::Agility).getModified() / 5.0f) + (getAttribute(ESM::Attribute::Luck).getModified() / 10.0f); evasion *= getFatigueTerm(); - evasion += mMagicEffects.get(ESM::MagicEffect::Sanctuary).mMagnitude; + evasion += mMagicEffects.get(ESM::MagicEffect::Sanctuary).getMagnitude(); return evasion; } @@ -371,6 +368,11 @@ namespace MWMechanics return false; } + void CreatureStats::setNeedRecalcDynamicStats(bool val) + { + mRecalcDynamicStats = val; + } + void CreatureStats::setKnockedDown(bool value) { mKnockdown = value; @@ -479,11 +481,11 @@ namespace MWMechanics state.mDead = mDead; state.mDied = mDied; + state.mMurdered = mMurdered; state.mFriendlyHits = mFriendlyHits; state.mTalkedTo = mTalkedTo; state.mAlarmed = mAlarmed; state.mAttacked = mAttacked; - state.mHostile = mHostile; state.mAttackingOrSpell = mAttackingOrSpell; // TODO: rewrite. does this really need 3 separate bools? state.mKnockdown = mKnockdown; @@ -503,6 +505,15 @@ namespace MWMechanics mSpells.writeState(state.mSpells); mActiveSpells.writeState(state.mActiveSpells); + mAiSequence.writeState(state.mAiSequence); + mMagicEffects.writeState(state.mMagicEffects); + + state.mSummonedCreatureMap = mSummonedCreatures; + state.mSummonGraveyard = mSummonGraveyard; + + state.mHasAiSettings = true; + for (int i=0; i<4; ++i) + mAiSettings[i].writeState (state.mAiSettings[i]); } void CreatureStats::readState (const ESM::CreatureStats& state) @@ -515,15 +526,14 @@ namespace MWMechanics mLastRestock = MWWorld::TimeStamp(state.mTradeTime); mGoldPool = state.mGoldPool; - mFallHeight = state.mFallHeight; mDead = state.mDead; mDied = state.mDied; + mMurdered = state.mMurdered; mFriendlyHits = state.mFriendlyHits; mTalkedTo = state.mTalkedTo; mAlarmed = state.mAlarmed; mAttacked = state.mAttacked; - mHostile = state.mHostile; mAttackingOrSpell = state.mAttackingOrSpell; // TODO: rewrite. does this really need 3 separate bools? mKnockdown = state.mKnockdown; @@ -543,6 +553,15 @@ namespace MWMechanics mSpells.readState(state.mSpells); mActiveSpells.readState(state.mActiveSpells); + mAiSequence.readState(state.mAiSequence); + mMagicEffects.readState(state.mMagicEffects); + + mSummonedCreatures = state.mSummonedCreatureMap; + mSummonGraveyard = state.mSummonGraveyard; + + if (state.mHasAiSettings) + for (int i=0; i<4; ++i) + mAiSettings[i].readState(state.mAiSettings[i]); } void CreatureStats::setLastRestockTime(MWWorld::TimeStamp tradeTime) @@ -603,4 +622,14 @@ namespace MWMechanics { mDeathAnimation = index; } + + std::map& CreatureStats::getSummonedCreatureMap() + { + return mSummonedCreatures; + } + + std::vector& CreatureStats::getSummonedCreatureGraveyard() + { + return mSummonGraveyard; + } } diff --git a/apps/openmw/mwmechanics/creaturestats.hpp b/apps/openmw/mwmechanics/creaturestats.hpp index 8b2398dfb..5e169ffb0 100644 --- a/apps/openmw/mwmechanics/creaturestats.hpp +++ b/apps/openmw/mwmechanics/creaturestats.hpp @@ -35,11 +35,11 @@ namespace MWMechanics AiSequence mAiSequence; bool mDead; bool mDied; + bool mMurdered; int mFriendlyHits; bool mTalkedTo; bool mAlarmed; bool mAttacked; - bool mHostile; bool mAttackingOrSpell; bool mKnockdown; bool mKnockdownOneFrame; @@ -67,6 +67,12 @@ namespace MWMechanics // The index of the death animation that was played unsigned char mDeathAnimation; + // + std::map mSummonedCreatures; + // Contains ActorIds of summoned creatures with an expired lifetime that have not been deleted yet. + // This may be necessary when the creature is in an inactive cell. + std::vector mSummonGraveyard; + protected: // These two are only set by NpcStats, but they are declared in CreatureStats to prevent using virtual methods. bool mIsWerewolf; @@ -85,6 +91,7 @@ namespace MWMechanics void setAttackStrength(float value); bool needToRecalcDynamicStats(); + void setNeedRecalcDynamicStats(bool val); void addToFallHeight(float height); @@ -130,11 +137,8 @@ namespace MWMechanics void setDynamic (int index, const DynamicStat &value); - void setSpells(const Spells &spells); - - void setActiveSpells(const ActiveSpells &active); - - void setMagicEffects(const MagicEffects &effects); + /// Set Modifier for each magic effect according to \a effects. Does not touch Base values. + void modifyMagicEffects(const MagicEffects &effects); void setAttackingOrSpell(bool attackingOrSpell); @@ -160,10 +164,18 @@ namespace MWMechanics bool isDead() const; + void notifyDied(); + bool hasDied() const; void clearHasDied(); + bool hasBeenMurdered() const; + + void clearHasBeenMurdered(); + + void notifyMurder(); + void resurrect(); bool hasCommonDisease() const; @@ -187,15 +199,13 @@ namespace MWMechanics bool getAttacked() const; void setAttacked (bool attacked); - bool isHostile() const; - void setHostile (bool hostile); - bool getCreatureTargetted() const; float getEvasion() const; void setKnockedDown(bool value); - ///Returns true for the entire duration of the actor being knocked down + /// Returns true for the entire duration of the actor being knocked down or knocked out, + /// including transition animations (falling down & standing up) bool getKnockedDown() const; void setKnockedDownOneFrame(bool value); ///Returns true only for the first frame of the actor being knocked out; used for "onKnockedOut" command @@ -208,12 +218,17 @@ namespace MWMechanics void setBlock(bool value); bool getBlock() const; + std::map& getSummonedCreatureMap(); + std::vector& getSummonedCreatureGraveyard(); + enum Flag { Flag_ForceRun = 1, Flag_ForceSneak = 2, Flag_Run = 4, - Flag_Sneak = 8 + Flag_Sneak = 8, + Flag_ForceJump = 16, + Flag_ForceMoveJump = 32 }; enum Stance { @@ -233,13 +248,6 @@ namespace MWMechanics // TODO: Put it somewhere else? std::set mBoundItems; - // TODO: store in savegame - // TODO: encapsulate? - // - std::map mSummonedCreatures; - // Contains summoned creatures with an expired lifetime that have not been deleted yet. - std::vector mSummonGraveyard; - void writeState (ESM::CreatureStats& state) const; void readState (const ESM::CreatureStats& state); diff --git a/apps/openmw/mwmechanics/difficultyscaling.cpp b/apps/openmw/mwmechanics/difficultyscaling.cpp new file mode 100644 index 000000000..05ab12ccd --- /dev/null +++ b/apps/openmw/mwmechanics/difficultyscaling.cpp @@ -0,0 +1,38 @@ +#include "difficultyscaling.hpp" + +#include "../mwbase/world.hpp" +#include "../mwbase/environment.hpp" +#include "../mwworld/esmstore.hpp" + +#include + +float scaleDamage(float damage, const MWWorld::Ptr& attacker, const MWWorld::Ptr& victim) +{ + const MWWorld::Ptr& player = MWBase::Environment::get().getWorld()->getPlayerPtr(); + + // [-100, 100] + int difficultySetting = Settings::Manager::getInt("difficulty", "Game"); + + static const float fDifficultyMult = MWBase::Environment::get().getWorld()->getStore().get().find("fDifficultyMult")->getFloat(); + + float difficultyTerm = 0.01f * difficultySetting; + + float x = 0; + if (victim == player) + { + if (difficultyTerm > 0) + x = fDifficultyMult * difficultyTerm; + else + x = difficultyTerm / fDifficultyMult; + } + else if (attacker == player) + { + if (difficultyTerm > 0) + x = -difficultyTerm / fDifficultyMult; + else + x = fDifficultyMult * (-difficultyTerm); + } + + damage *= 1 + x; + return damage; +} diff --git a/apps/openmw/mwmechanics/difficultyscaling.hpp b/apps/openmw/mwmechanics/difficultyscaling.hpp new file mode 100644 index 000000000..168cf1055 --- /dev/null +++ b/apps/openmw/mwmechanics/difficultyscaling.hpp @@ -0,0 +1,12 @@ +#ifndef OPENMW_MWMECHANICS_DIFFICULTYSCALING_H +#define OPENMW_MWMECHANICS_DIFFICULTYSCALING_H + +namespace MWWorld +{ + class Ptr; +} + +/// Scales damage dealt to an actor based on difficulty setting +float scaleDamage(float damage, const MWWorld::Ptr& attacker, const MWWorld::Ptr& victim); + +#endif diff --git a/apps/openmw/mwmechanics/disease.hpp b/apps/openmw/mwmechanics/disease.hpp index 5f73e8acd..a973c0e35 100644 --- a/apps/openmw/mwmechanics/disease.hpp +++ b/apps/openmw/mwmechanics/disease.hpp @@ -14,37 +14,53 @@ namespace MWMechanics { /// Call when \a actor has got in contact with \a carrier (e.g. hit by him, or loots him) + /// @param actor The actor that will potentially catch diseases. Currently only the player can catch diseases. + /// @param carrier The disease carrier. inline void diseaseContact (MWWorld::Ptr actor, MWWorld::Ptr carrier) { - if (!carrier.getClass().isActor()) + if (!carrier.getClass().isActor() || actor != MWBase::Environment::get().getWorld()->getPlayerPtr()) return; float fDiseaseXferChance = MWBase::Environment::get().getWorld()->getStore().get().find( "fDiseaseXferChance")->getFloat(); + MagicEffects& actorEffects = actor.getClass().getCreatureStats(actor).getMagicEffects(); + Spells& spells = carrier.getClass().getCreatureStats(carrier).getSpells(); for (Spells::TIterator it = spells.begin(); it != spells.end(); ++it) { const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().find(it->first); - if (spell->mData.mType == ESM::Spell::ST_Disease - || spell->mData.mType == ESM::Spell::ST_Blight) + + if (actor.getClass().getCreatureStats(actor).getSpells().hasSpell(spell->mId)) + continue; + + float resist = 0.f; + if (spells.hasCorprusEffect(spell)) + resist = 1.f - 0.01 * (actorEffects.get(ESM::MagicEffect::ResistCorprusDisease).getMagnitude() + - actorEffects.get(ESM::MagicEffect::WeaknessToCorprusDisease).getMagnitude()); + else if (spell->mData.mType == ESM::Spell::ST_Disease) + resist = 1.f - 0.01 * (actorEffects.get(ESM::MagicEffect::ResistCommonDisease).getMagnitude() + - actorEffects.get(ESM::MagicEffect::WeaknessToCommonDisease).getMagnitude()); + else if (spell->mData.mType == ESM::Spell::ST_Blight) + resist = 1.f - 0.01 * (actorEffects.get(ESM::MagicEffect::ResistBlightDisease).getMagnitude() + - actorEffects.get(ESM::MagicEffect::WeaknessToBlightDisease).getMagnitude()); + else + continue; + + int x = fDiseaseXferChance * 100 * resist; + float roll = std::rand()/ (static_cast (RAND_MAX) + 1) * 10000; // [0, 9999] + + if (roll < x) { - float roll = std::rand()/ (static_cast (RAND_MAX) + 1) * 100; // [0, 99] - if (roll < fDiseaseXferChance) - { - // Contracted disease! - actor.getClass().getCreatureStats(actor).getSpells().add(it->first); - - if (actor.getRefData().getHandle() == "player") - { - std::string msg = "sMagicContractDisease"; - msg = MWBase::Environment::get().getWorld()->getStore().get().find(msg)->getString(); - if (msg.find("%s") != std::string::npos) - msg.replace(msg.find("%s"), 2, spell->mName); - MWBase::Environment::get().getWindowManager()->messageBox(msg); - } - } + // Contracted disease! + actor.getClass().getCreatureStats(actor).getSpells().add(it->first); + + std::string msg = "sMagicContractDisease"; + msg = MWBase::Environment::get().getWorld()->getStore().get().find(msg)->getString(); + if (msg.find("%s") != std::string::npos) + msg.replace(msg.find("%s"), 2, spell->mName); + MWBase::Environment::get().getWindowManager()->messageBox(msg); } } } diff --git a/apps/openmw/mwmechanics/enchanting.cpp b/apps/openmw/mwmechanics/enchanting.cpp index f3f6795db..c134942b8 100644 --- a/apps/openmw/mwmechanics/enchanting.cpp +++ b/apps/openmw/mwmechanics/enchanting.cpp @@ -53,6 +53,9 @@ namespace MWMechanics MWWorld::ContainerStore& store = player.getClass().getContainerStore(player); ESM::Enchantment enchantment; enchantment.mData.mCharge = getGemCharge(); + enchantment.mData.mAutocalc = 0; + enchantment.mData.mType = mCastStyle; + enchantment.mData.mCost = getEnchantPoints(); store.remove(mSoulGemPtr, 1, player); @@ -62,7 +65,7 @@ namespace MWMechanics if(mSelfEnchanting) { - if(getEnchantChance() (RAND_MAX)*100) + if(std::rand()/static_cast (RAND_MAX)*100 < getEnchantChance()) return false; mEnchanter.getClass().skillUsageSucceeded (mEnchanter, ESM::Skill::Enchant, 2); @@ -72,8 +75,6 @@ namespace MWMechanics { enchantment.mData.mCharge=0; } - enchantment.mData.mType = mCastStyle; - enchantment.mData.mCost = getEnchantPoints(); enchantment.mEffects = mEffectList; // Apply the enchantment @@ -166,16 +167,15 @@ namespace MWMechanics float enchantmentCost = 0; int effectsLeftCnt = mEffects.size(); - float baseCost, magnitudeCost, areaCost; - int magMin, magMax, area; for (std::vector::const_iterator it = mEffects.begin(); it != mEffects.end(); ++it) { - baseCost = (store.get().find(it->mEffectID))->mData.mBaseCost; + float baseCost = (store.get().find(it->mEffectID))->mData.mBaseCost; // To reflect vanilla behavior - magMin = (it->mMagnMin == 0) ? 1 : it->mMagnMin; - magMax = (it->mMagnMax == 0) ? 1 : it->mMagnMax; - area = (it->mArea == 0) ? 1 : it->mArea; + int magMin = (it->mMagnMin == 0) ? 1 : it->mMagnMin; + int magMax = (it->mMagnMax == 0) ? 1 : it->mMagnMax; + int area = (it->mArea == 0) ? 1 : it->mArea; + float magnitudeCost = 0; if (mCastStyle == ESM::Enchantment::ConstantEffect) { magnitudeCost = (magMin + magMax) * baseCost * 2.5; @@ -187,7 +187,7 @@ namespace MWMechanics magnitudeCost *= 1.5; } - areaCost = area * 0.025 * baseCost; + float areaCost = area * 0.025 * baseCost; if (it->mRange == ESM::RT_Target) areaCost *= 1.5; @@ -292,5 +292,9 @@ namespace MWMechanics MWWorld::ContainerStore& store = player.getClass().getContainerStore(player); store.remove(MWWorld::ContainerStore::sGoldId, getEnchantPrice(), player); + + // add gold to NPC trading gold pool + CreatureStats& enchanterStats = mEnchanter.getClass().getCreatureStats(mEnchanter); + enchanterStats.setGoldPool(enchanterStats.getGoldPool() + getEnchantPrice()); } } diff --git a/apps/openmw/mwmechanics/magiceffects.cpp b/apps/openmw/mwmechanics/magiceffects.cpp index 5be0854ab..0b19df0a8 100644 --- a/apps/openmw/mwmechanics/magiceffects.cpp +++ b/apps/openmw/mwmechanics/magiceffects.cpp @@ -6,6 +6,7 @@ #include #include +#include namespace MWMechanics { @@ -40,20 +41,57 @@ namespace MWMechanics return left.mArgsecond.setModifier(effects.get(it->first).getModifier()); + } + + for (Collection::const_iterator it = effects.begin(); it != effects.end(); ++it) + { + mCollection[it->first].setModifier(it->second.getModifier()); + } + } + MagicEffects& MagicEffects::operator+= (const MagicEffects& effects) { if (this==&effects) @@ -137,4 +193,25 @@ namespace MWMechanics return result; } + + void MagicEffects::writeState(ESM::MagicEffects &state) const + { + // Don't need to save Modifiers, they are recalculated every frame anyway. + for (Collection::const_iterator iter (begin()); iter!=end(); ++iter) + { + if (iter->second.getBase() != 0) + { + // Don't worry about mArg, never used by magic effect script instructions + state.mEffects.insert(std::make_pair(iter->first.mId, iter->second.getBase())); + } + } + } + + void MagicEffects::readState(const ESM::MagicEffects &state) + { + for (std::map::const_iterator it = state.mEffects.begin(); it != state.mEffects.end(); ++it) + { + mCollection[EffectKey(it->first)].setBase(it->second); + } + } } diff --git a/apps/openmw/mwmechanics/magiceffects.hpp b/apps/openmw/mwmechanics/magiceffects.hpp index 4fd5e159a..0a8392dab 100644 --- a/apps/openmw/mwmechanics/magiceffects.hpp +++ b/apps/openmw/mwmechanics/magiceffects.hpp @@ -8,6 +8,8 @@ namespace ESM { struct ENAMstruct; struct EffectList; + + struct MagicEffects; } namespace MWMechanics @@ -28,12 +30,27 @@ namespace MWMechanics struct EffectParam { - // Note usually this would be int, but applying partial resistance might introduce decimal point. - float mMagnitude; + private: + // Note usually this would be int, but applying partial resistance might introduce a decimal point. + float mModifier; + + int mBase; + + public: + /// Get the total magnitude including base and modifier. + float getMagnitude() const; + + void setModifier(float mod); + float getModifier() const; + + /// Change mBase by \a diff + void modifyBase(int diff); + void setBase(int base); + int getBase() const; EffectParam(); - EffectParam(float magnitude) : mMagnitude(magnitude) {} + EffectParam(float magnitude) : mModifier(magnitude), mBase(0) {} EffectParam& operator+= (const EffectParam& param); @@ -77,7 +94,16 @@ namespace MWMechanics Collection::const_iterator end() const { return mCollection.end(); } + void readState (const ESM::MagicEffects& state); + void writeState (ESM::MagicEffects& state) const; + void add (const EffectKey& key, const EffectParam& param); + void remove (const EffectKey& key); + + void modifyBase (const EffectKey& key, int diff); + + /// Copy Modifier values from \a effects, but keep original mBase values. + void setModifiers(const MagicEffects& effects); MagicEffects& operator+= (const MagicEffects& effects); diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp index 900ea72ca..f7949f1ec 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp @@ -14,34 +14,14 @@ #include "../mwworld/player.hpp" #include "../mwmechanics/aicombat.hpp" +#include "../mwmechanics/aipursue.hpp" #include #include "spellcasting.hpp" +#include "autocalcspell.hpp" -namespace -{ - /// @return is \a ptr allowed to take/use \a item or is it a crime? - bool isAllowedToUse (const MWWorld::Ptr& ptr, const MWWorld::Ptr& item, MWWorld::Ptr& victim) - { - const std::string& owner = item.getCellRef().getOwner(); - bool isOwned = !owner.empty() && owner != "player"; - - const std::string& faction = item.getCellRef().getFaction(); - bool isFactionOwned = false; - if (!faction.empty() && ptr.getClass().isNpc()) - { - const std::map& factions = ptr.getClass().getNpcStats(ptr).getFactionRanks(); - if (factions.find(Misc::StringUtils::lowerCase(faction)) == factions.end()) - isFactionOwned = true; - } - - if (!item.getCellRef().getOwner().empty()) - victim = MWBase::Environment::get().getWorld()->searchPtr(item.getCellRef().getOwner(), true); - - return (!isOwned && !isFactionOwned); - } -} +#include namespace MWMechanics { @@ -57,7 +37,7 @@ namespace MWMechanics // reset creatureStats.setLevel(player->mNpdt52.mLevel); creatureStats.getSpells().clear(); - creatureStats.setMagicEffects(MagicEffects()); + creatureStats.modifyMagicEffects(MagicEffects()); for (int i=0; i<27; ++i) npcStats.getSkill (i).setBase (player->mNpdt52.mSkills[i]); @@ -154,19 +134,6 @@ namespace MWMechanics npcStats.getSkill (index).setBase ( npcStats.getSkill (index).getBase() + bonus); } - - if (i==1) - { - // Major skill - add starting spells for this skill if existing - const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); - MWWorld::Store::iterator it = store.get().begin(); - for (; it != store.get().end(); ++it) - { - if (it->mData.mFlags & ESM::Spell::F_PCStart - && spellSchoolToSkill(getSpellSchool(&*it, ptr)) == index) - creatureStats.getSpells().add(it->mId); - } - } } } @@ -189,6 +156,87 @@ namespace MWMechanics } } + // F_PCStart spells + static const float fPCbaseMagickaMult = esmStore.get().find("fPCbaseMagickaMult")->getFloat(); + + float baseMagicka = fPCbaseMagickaMult * creatureStats.getAttribute(ESM::Attribute::Intelligence).getBase(); + bool reachedLimit = false; + const ESM::Spell* weakestSpell = NULL; + int minCost = INT_MAX; + + std::vector selectedSpells; + + const ESM::Race* race = NULL; + if (mRaceSelected) + race = esmStore.get().find(player->mRace); + + int skills[ESM::Skill::Length]; + for (int i=0; i &spells = + esmStore.get(); + for (MWWorld::Store::iterator iter = spells.begin(); iter != spells.end(); ++iter) + { + const ESM::Spell* spell = &*iter; + + if (spell->mData.mType != ESM::Spell::ST_Spell) + continue; + if (!(spell->mData.mFlags & ESM::Spell::F_PCStart)) + continue; + if (reachedLimit && spell->mData.mCost <= minCost) + continue; + if (race && std::find(race->mPowers.mList.begin(), race->mPowers.mList.end(), spell->mId) != race->mPowers.mList.end()) + continue; + if (baseMagicka < spell->mData.mCost) + continue; + + static const float fAutoPCSpellChance = esmStore.get().find("fAutoPCSpellChance")->getFloat(); + if (calcAutoCastChance(spell, skills, attributes, -1) < fAutoPCSpellChance) + continue; + + if (!attrSkillCheck(spell, skills, attributes)) + continue; + + selectedSpells.push_back(spell->mId); + + if (reachedLimit) + { + std::vector::iterator it = std::find(selectedSpells.begin(), selectedSpells.end(), weakestSpell->mId); + if (it != selectedSpells.end()) + selectedSpells.erase(it); + + minCost = INT_MAX; + for (std::vector::iterator weakIt = selectedSpells.begin(); weakIt != selectedSpells.end(); ++weakIt) + { + const ESM::Spell* testSpell = esmStore.get().find(*weakIt); + if (testSpell->mData.mCost < minCost) + { + minCost = testSpell->mData.mCost; + weakestSpell = testSpell; + } + } + } + else + { + if (spell->mData.mCost < minCost) + { + weakestSpell = spell; + minCost = weakestSpell->mData.mCost; + } + static const unsigned int iAutoPCSpellMax = esmStore.get().find("iAutoPCSpellMax")->getInt(); + if (selectedSpells.size() == iAutoPCSpellMax) + reachedLimit = true; + } + } + + for (std::vector::iterator it = selectedSpells.begin(); it != selectedSpells.end(); ++it) + creatureStats.getSpells().add(*it); + // forced update and current value adjustments mActors.updateActor (ptr, 0); @@ -207,7 +255,7 @@ namespace MWMechanics } MechanicsManager::MechanicsManager() - : mUpdatePlayer (true), mClassSelected (false), + : mWatchedStatsEmpty (true), mUpdatePlayer (true), mClassSelected (false), mRaceSelected (false), mAI(true) { //buildPlayer no longer here, needs to be done explicitely after all subsystems are up and running @@ -269,7 +317,7 @@ namespace MWMechanics const MWMechanics::NpcStats &stats = mWatched.getClass().getNpcStats(mWatched); for(int i = 0;i < ESM::Attribute::Length;++i) { - if(stats.getAttribute(i) != mWatchedStats.getAttribute(i)) + if(stats.getAttribute(i) != mWatchedStats.getAttribute(i) || mWatchedStatsEmpty) { std::stringstream attrname; attrname << "AttribVal"<<(i+1); @@ -279,19 +327,19 @@ namespace MWMechanics } } - if(stats.getHealth() != mWatchedStats.getHealth()) + if(stats.getHealth() != mWatchedStats.getHealth() || mWatchedStatsEmpty) { static const std::string hbar("HBar"); mWatchedStats.setHealth(stats.getHealth()); winMgr->setValue(hbar, stats.getHealth()); } - if(stats.getMagicka() != mWatchedStats.getMagicka()) + if(stats.getMagicka() != mWatchedStats.getMagicka() || mWatchedStatsEmpty) { static const std::string mbar("MBar"); mWatchedStats.setMagicka(stats.getMagicka()); winMgr->setValue(mbar, stats.getMagicka()); } - if(stats.getFatigue() != mWatchedStats.getFatigue()) + if(stats.getFatigue() != mWatchedStats.getFatigue() || mWatchedStatsEmpty) { static const std::string fbar("FBar"); mWatchedStats.setFatigue(stats.getFatigue()); @@ -317,7 +365,7 @@ namespace MWMechanics //Loop over ESM::Skill::SkillEnum for(int i = 0; i < ESM::Skill::Length; ++i) { - if(stats.getSkill(i) != mWatchedStats.getSkill(i)) + if(stats.getSkill(i) != mWatchedStats.getSkill(i) || mWatchedStatsEmpty) { update = true; mWatchedStats.getSkill(i) = stats.getSkill(i); @@ -330,6 +378,8 @@ namespace MWMechanics winMgr->setValue("level", stats.getLevel()); + mWatchedStatsEmpty = false; + // Update the equipped weapon icon MWWorld::InventoryStore& inv = mWatched.getClass().getInventoryStore(mWatched); MWWorld::ContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); @@ -483,11 +533,14 @@ namespace MWMechanics MWWorld::LiveCellRef* player = playerPtr.get(); const MWMechanics::NpcStats &playerStats = playerPtr.getClass().getNpcStats(playerPtr); + const MWWorld::Store& gmst = MWBase::Environment::get().getWorld()->getStore().get(); + static const float fDispRaceMod = gmst.find("fDispRaceMod")->getFloat(); if (Misc::StringUtils::ciEqual(npc->mBase->mRace, player->mBase->mRace)) - x += MWBase::Environment::get().getWorld()->getStore().get().find("fDispRaceMod")->getFloat(); + x += fDispRaceMod; - x += MWBase::Environment::get().getWorld()->getStore().get().find("fDispPersonalityMult")->getFloat() - * (playerStats.getAttribute(ESM::Attribute::Personality).getModified() - MWBase::Environment::get().getWorld()->getStore().get().find("fDispPersonalityBase")->getFloat()); + static const float fDispPersonalityMult = gmst.find("fDispPersonalityMult")->getFloat(); + static const float fDispPersonalityBase = gmst.find("fDispPersonalityBase")->getFloat(); + x += fDispPersonalityMult * (playerStats.getAttribute(ESM::Attribute::Personality).getModified() - fDispPersonalityBase); float reaction = 0; int rank = 0; @@ -500,7 +553,8 @@ namespace MWMechanics { if (!playerStats.getExpelled(npcFaction)) { - reaction = playerStats.getFactionReputation(npcFaction); + // faction reaction towards itself. yes, that exists + reaction = MWBase::Environment::get().getDialogueManager()->getFactionReaction(npcFaction, npcFaction); rank = playerStats.getFactionRanks().find(npcFaction)->second; } @@ -522,18 +576,25 @@ namespace MWMechanics reaction = 0; rank = 0; } - x += (MWBase::Environment::get().getWorld()->getStore().get().find("fDispFactionRankMult")->getFloat() * rank - + MWBase::Environment::get().getWorld()->getStore().get().find("fDispFactionRankBase")->getFloat()) - * MWBase::Environment::get().getWorld()->getStore().get().find("fDispFactionMod")->getFloat() * reaction; - x -= MWBase::Environment::get().getWorld()->getStore().get().find("fDispCrimeMod")->getFloat() * playerStats.getBounty(); + static const float fDispFactionRankMult = gmst.find("fDispFactionRankMult")->getFloat(); + static const float fDispFactionRankBase = gmst.find("fDispFactionRankBase")->getFloat(); + static const float fDispFactionMod = gmst.find("fDispFactionMod")->getFloat(); + x += (fDispFactionRankMult * rank + + fDispFactionRankBase) + * fDispFactionMod * reaction; + + static const float fDispCrimeMod = gmst.find("fDispCrimeMod")->getFloat(); + static const float fDispDiseaseMod = gmst.find("fDispDiseaseMod")->getFloat(); + x -= fDispCrimeMod * playerStats.getBounty(); if (playerStats.hasCommonDisease() || playerStats.hasBlightDisease()) - x += MWBase::Environment::get().getWorld()->getStore().get().find("fDispDiseaseMod")->getFloat(); + x += fDispDiseaseMod; + static const float fDispWeaponDrawn = gmst.find("fDispWeaponDrawn")->getFloat(); if (playerStats.getDrawState() == MWMechanics::DrawState_Weapon) - x += MWBase::Environment::get().getWorld()->getStore().get().find("fDispWeaponDrawn")->getFloat(); + x += fDispWeaponDrawn; - x += ptr.getClass().getCreatureStats(ptr).getMagicEffects().get(ESM::MagicEffect::Charm).mMagnitude; + x += ptr.getClass().getCreatureStats(ptr).getMagicEffects().get(ESM::MagicEffect::Charm).getMagnitude(); int effective_disposition = std::max(0,std::min(int(x),100));//, normally clamped to [0..100] when used return effective_disposition; @@ -582,7 +643,6 @@ namespace MWMechanics return mActors.countDeaths (id); } - void MechanicsManager::getPersuasionDispositionChange (const MWWorld::Ptr& npc, PersuasionType type, float currentTemporaryDispositionDelta, bool& success, float& tempChange, float& permChange) { @@ -786,8 +846,43 @@ namespace MWMechanics mAI = true; } + bool MechanicsManager::isAllowedToUse (const MWWorld::Ptr& ptr, const MWWorld::Ptr& item, MWWorld::Ptr& victim) + { + const std::string& owner = item.getCellRef().getOwner(); + bool isOwned = !owner.empty() && owner != "player"; + + const std::string& faction = item.getCellRef().getFaction(); + bool isFactionOwned = false; + if (!faction.empty() && ptr.getClass().isNpc()) + { + const std::map& factions = ptr.getClass().getNpcStats(ptr).getFactionRanks(); + std::map::const_iterator found = factions.find(Misc::StringUtils::lowerCase(faction)); + if (found == factions.end() + || found->second < item.getCellRef().getFactionRank()) + isFactionOwned = true; + } + + const std::string& globalVariable = item.getCellRef().getGlobalVariable(); + if (!globalVariable.empty() && MWBase::Environment::get().getWorld()->getGlobalInt(Misc::StringUtils::lowerCase(globalVariable)) == 1) + { + isOwned = false; + isFactionOwned = false; + } + + if (!item.getCellRef().getOwner().empty()) + victim = MWBase::Environment::get().getWorld()->searchPtr(item.getCellRef().getOwner(), true); + + return (!isOwned && !isFactionOwned); + } + bool MechanicsManager::sleepInBed(const MWWorld::Ptr &ptr, const MWWorld::Ptr &bed) { + if (ptr.getClass().getNpcStats(ptr).isWerewolf()) + { + MWBase::Environment::get().getWindowManager()->messageBox("#{sWerewolfRefusal}"); + return true; + } + if(MWBase::Environment::get().getWorld()->getPlayer().isInCombat()) { MWBase::Environment::get().getWindowManager()->messageBox("#{sNotifyMessage2}"); return true; @@ -827,6 +922,8 @@ namespace MWMechanics // NOTE: int arg can be from itemTaken() so DON'T modify it, since it is // passed to reportCrime later on in this function. + // NOTE: victim may be empty + // Only player can commit crime if (player.getRefData().getHandle() != "player") return false; @@ -834,7 +931,7 @@ namespace MWMechanics const MWWorld::ESMStore& esmStore = MWBase::Environment::get().getWorld()->getStore(); // What amount of alarm did this crime generate? - int alarm; + int alarm = 0; if (type == OT_Trespassing || type == OT_SleepingInOwnedBed) alarm = esmStore.get().find("iAlarmTresspass")->getInt(); else if (type == OT_Pickpocket) @@ -845,80 +942,74 @@ namespace MWMechanics alarm = esmStore.get().find("iAlarmKilling")->getInt(); else if (type == OT_Theft) alarm = esmStore.get().find("iAlarmStealing")->getInt(); - else - return false; - // Innocent until proven guilty bool reported = false; // Find all the actors within the alarm radius std::vector neighbors; - mActors.getObjectsInRange(Ogre::Vector3(player.getRefData().getPosition().pos), - esmStore.get().find("fAlarmRadius")->getInt(), neighbors); - int id = MWBase::Environment::get().getWorld()->getPlayer().getNewCrimeId(); + Ogre::Vector3 from = Ogre::Vector3(player.getRefData().getPosition().pos); + float radius = esmStore.get().find("fAlarmRadius")->getFloat(); + + mActors.getObjectsInRange(from, radius, neighbors); + + // victim should be considered even beyond alarm radius + if (!victim.isEmpty() && from.squaredDistance(Ogre::Vector3(victim.getRefData().getPosition().pos)) > radius*radius) + neighbors.push_back(victim); - // Find actors who witnessed the crime + bool victimAware = false; + + // Find actors who directly witnessed the crime for (std::vector::iterator it = neighbors.begin(); it != neighbors.end(); ++it) { - if (*it == player) continue; // not the player + if (*it == player) + continue; // skip player + if (it->getClass().getCreatureStats(*it).isDead()) + continue; // Was the crime seen? - if (MWBase::Environment::get().getWorld()->getLOS(player, *it) && awarenessCheck(player, *it) ) + if ((MWBase::Environment::get().getWorld()->getLOS(player, *it) && awarenessCheck(player, *it) ) + // Murder crime can be reported even if no one saw it (hearing is enough, I guess). + // TODO: Add mod support for stealth executions! + || (type == OT_Murder && *it != victim)) { - // TODO: Add more messages + if (*it == victim) + victimAware = true; + + // TODO: are there other messages? if (type == OT_Theft) MWBase::Environment::get().getDialogueManager()->say(*it, "thief"); - if (*it == victim) - { - // Self-defense - // The victim is aware of the criminal/assailant. If being assaulted, fight back now - // (regardless of whether the assault is reported or not) - // This applies to both NPCs and creatures - - // ... except if this is a guard: then the player is given a chance to pay a fine / go to jail instead - if (type == OT_Assault && !it->getClass().isClass(*it, "guard")) - MWBase::Environment::get().getMechanicsManager()->startCombat(victim, player); - } - // Crime reporting only applies to NPCs if (!it->getClass().isNpc()) continue; + if (it->getClass().getCreatureStats(*it).getAiSequence().isInCombat(victim)) + continue; + // Will the witness report the crime? if (it->getClass().getCreatureStats(*it).getAiSetting(CreatureStats::AI_Alarm).getBase() >= alarm) { reported = true; - - // Tell everyone, including yourself - for (std::vector::iterator it1 = neighbors.begin(); it1 != neighbors.end(); ++it1) - { - if ( *it1 == player - || !it1->getClass().isNpc()) continue; // not the player and is an NPC - - // Will other witnesses paticipate in crime - if ( it1->getClass().getCreatureStats(*it1).getAiSetting(CreatureStats::AI_Alarm).getBase() >= alarm - || type == OT_Assault ) - { - it1->getClass().getNpcStats(*it1).setCrimeId(id); - } - - // Mark as Alarmed for dialogue - it1->getClass().getCreatureStats(*it1).setAlarmed(true); - } } } } + if (reported) reportCrime(player, victim, type, arg); + else if (victimAware && !victim.isEmpty() && type == OT_Assault) + startCombat(victim, player); + return reported; } - void MechanicsManager::reportCrime(const MWWorld::Ptr &ptr, const MWWorld::Ptr &victim, OffenseType type, int arg) + void MechanicsManager::reportCrime(const MWWorld::Ptr &player, const MWWorld::Ptr &victim, OffenseType type, int arg) { const MWWorld::Store& store = MWBase::Environment::get().getWorld()->getStore().get(); + if (type == OT_Murder && !victim.isEmpty()) + victim.getClass().getCreatureStats(victim).notifyMurder(); + // Bounty for each type of crime if (type == OT_Trespassing || type == OT_SleepingInOwnedBed) arg = store.find("iCrimeTresspass")->getInt(); @@ -929,10 +1020,13 @@ namespace MWMechanics else if (type == OT_Murder) arg = store.find("iCrimeKilling")->getInt(); else if (type == OT_Theft) + { arg *= store.find("fCrimeStealing")->getFloat(); + arg = std::max(1, arg); // Minimum bounty of 1, in case items with zero value are stolen + } MWBase::Environment::get().getWindowManager()->messageBox("#{sCrimeMessage}"); - ptr.getClass().getNpcStats(ptr).setBounty(ptr.getClass().getNpcStats(ptr).getBounty() + player.getClass().getNpcStats(player).setBounty(player.getClass().getNpcStats(player).getBounty() + arg); // If committing a crime against a faction member, expell from the faction @@ -941,11 +1035,132 @@ namespace MWMechanics std::string factionID; if(!victim.getClass().getNpcStats(victim).getFactionRanks().empty()) factionID = victim.getClass().getNpcStats(victim).getFactionRanks().begin()->first; - if (ptr.getClass().getNpcStats(ptr).isSameFaction(victim.getClass().getNpcStats(victim))) + if (player.getClass().getNpcStats(player).isSameFaction(victim.getClass().getNpcStats(victim))) + { + player.getClass().getNpcStats(player).expell(factionID); + } + } + + // Make surrounding actors within alarm distance respond to the crime + std::vector neighbors; + + const MWWorld::ESMStore& esmStore = MWBase::Environment::get().getWorld()->getStore(); + + Ogre::Vector3 from = Ogre::Vector3(player.getRefData().getPosition().pos); + float radius = esmStore.get().find("fAlarmRadius")->getFloat(); + + mActors.getObjectsInRange(from, radius, neighbors); + + // victim should be considered even beyond alarm radius + if (!victim.isEmpty() && from.squaredDistance(Ogre::Vector3(victim.getRefData().getPosition().pos)) > radius*radius) + neighbors.push_back(victim); + + int id = MWBase::Environment::get().getWorld()->getPlayer().getNewCrimeId(); + + // What amount of provocation did this crime generate? + // Controls whether witnesses will engage combat with the criminal. + int fight = 0; + if (type == OT_Trespassing || type == OT_SleepingInOwnedBed) + fight = esmStore.get().find("iFightTrespass")->getInt(); + else if (type == OT_Pickpocket) + fight = esmStore.get().find("iFightPickpocket")->getInt(); + else if (type == OT_Assault) // Note: iFightAttack is for the victim, iFightAttacking for witnesses? + fight = esmStore.get().find("iFightAttack")->getInt(); + else if (type == OT_Murder) + fight = esmStore.get().find("iFightKilling")->getInt(); + else if (type == OT_Theft) + fight = esmStore.get().find("fFightStealing")->getFloat(); + + const int iFightAttacking = esmStore.get().find("iFightAttacking")->getInt(); + + // Tell everyone (including the original reporter) in alarm range + for (std::vector::iterator it = neighbors.begin(); it != neighbors.end(); ++it) + { + if ( *it == player + || !it->getClass().isNpc() || it->getClass().getCreatureStats(*it).isDead()) continue; + + int aggression = fight; + + // Note: iFightAttack is used for the victim, iFightAttacking for witnesses? + if (*it != victim && type == OT_Assault) + aggression = iFightAttacking; + + if (it->getClass().getCreatureStats(*it).getAiSequence().isInCombat(victim)) + continue; + + if (it->getClass().isClass(*it, "guard")) + { + // Mark as Alarmed for dialogue + it->getClass().getCreatureStats(*it).setAlarmed(true); + + // Set the crime ID, which we will use to calm down participants + // once the bounty has been paid. + it->getClass().getNpcStats(*it).setCrimeId(id); + + it->getClass().getCreatureStats(*it).getAiSequence().stack(AiPursue(player), *it); + } + else + { + bool aggressive = MWBase::Environment::get().getMechanicsManager()->isAggressive(*it, player, aggression, true); + if (aggressive) + { + startCombat(*it, player); + + // Set the crime ID, which we will use to calm down participants + // once the bounty has been paid. + it->getClass().getNpcStats(*it).setCrimeId(id); + + // Mark as Alarmed for dialogue + it->getClass().getCreatureStats(*it).setAlarmed(true); + } + } + } + } + + bool MechanicsManager::actorAttacked(const MWWorld::Ptr &ptr, const MWWorld::Ptr &attacker) + { + if (ptr == MWBase::Environment::get().getWorld()->getPlayerPtr()) + return false; + + std::list followers = getActorsFollowing(attacker); + if (std::find(followers.begin(), followers.end(), ptr) != followers.end()) + { + ptr.getClass().getCreatureStats(ptr).friendlyHit(); + + if (ptr.getClass().getCreatureStats(ptr).getFriendlyHits() < 4) { - ptr.getClass().getNpcStats(ptr).expell(factionID); + MWBase::Environment::get().getDialogueManager()->say(ptr, "hit"); + return false; } } + + // Attacking an NPC that is already in combat with any other NPC is not a crime + AiSequence& seq = ptr.getClass().getCreatureStats(ptr).getAiSequence(); + bool isFightingNpc = false; + for (std::list::const_iterator it = seq.begin(); it != seq.end(); ++it) + { + if ((*it)->getTypeId() == AiPackage::TypeIdCombat) + { + MWWorld::Ptr target = static_cast(*it)->getTarget(); + if (!target.isEmpty() && target.getClass().isNpc()) + isFightingNpc = true; + } + } + + if (ptr.getClass().isNpc() && !attacker.isEmpty() && !ptr.getClass().getCreatureStats(ptr).getAiSequence().isInCombat(attacker) + && !isAggressive(ptr, attacker) && !isFightingNpc) + commitCrime(attacker, ptr, MWBase::MechanicsManager::OT_Assault); + + if (!attacker.isEmpty() && (attacker.getClass().getCreatureStats(attacker).getAiSequence().isInCombat(ptr) + || attacker == MWBase::Environment::get().getWorld()->getPlayerPtr()) + && !ptr.getClass().getCreatureStats(ptr).getAiSequence().isInCombat(attacker)) + { + // Attacker is in combat with us, but we are not in combat with the attacker yet. Time to fight back. + // Note: accidental or collateral damage attacks are ignored. + startCombat(ptr, attacker); + } + + return true; } bool MechanicsManager::awarenessCheck(const MWWorld::Ptr &ptr, const MWWorld::Ptr &observer) @@ -957,7 +1172,7 @@ namespace MWMechanics CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); - float invisibility = stats.getMagicEffects().get(ESM::MagicEffect::Invisibility).mMagnitude; + float invisibility = stats.getMagicEffects().get(ESM::MagicEffect::Invisibility).getMagnitude(); if (invisibility > 0) return false; @@ -989,13 +1204,13 @@ namespace MWMechanics Ogre::Vector3 pos2 (observer.getRefData().getPosition().pos); float distTerm = fSneakDistBase + fSneakDistMult * pos1.distance(pos2); - float chameleon = stats.getMagicEffects().get(ESM::MagicEffect::Chameleon).mMagnitude; + float chameleon = stats.getMagicEffects().get(ESM::MagicEffect::Chameleon).getMagnitude(); float x = sneakTerm * distTerm * stats.getFatigueTerm() + chameleon + invisibility; CreatureStats& observerStats = observer.getClass().getCreatureStats(observer); int obsAgility = observerStats.getAttribute(ESM::Attribute::Agility).getModified(); int obsLuck = observerStats.getAttribute(ESM::Attribute::Luck).getModified(); - float obsBlind = observerStats.getMagicEffects().get(ESM::MagicEffect::Blind).mMagnitude; + float obsBlind = observerStats.getMagicEffects().get(ESM::MagicEffect::Blind).getMagnitude(); int obsSneak = observer.getClass().getSkill(observer, ESM::Skill::Sneak); float obsTerm = obsSneak + 0.2 * obsAgility + 0.1 * obsLuck - obsBlind; @@ -1019,12 +1234,31 @@ namespace MWMechanics void MechanicsManager::startCombat(const MWWorld::Ptr &ptr, const MWWorld::Ptr &target) { + if (ptr.getClass().getCreatureStats(ptr).getAiSequence().isInCombat(target)) + return; ptr.getClass().getCreatureStats(ptr).getAiSequence().stack(MWMechanics::AiCombat(target), ptr); if (target == MWBase::Environment::get().getWorld()->getPlayerPtr()) - ptr.getClass().getCreatureStats(ptr).setHostile(true); + { + // if guard starts combat with player, guards pursuing player should do the same + if (ptr.getClass().isClass(ptr, "Guard")) + { + for (Actors::PtrControllerMap::const_iterator iter = mActors.begin(); iter != mActors.end(); ++iter) + { + if (iter->first.getClass().isClass(iter->first, "Guard")) + { + MWMechanics::AiSequence& aiSeq = iter->first.getClass().getCreatureStats(iter->first).getAiSequence(); + if (aiSeq.getTypeId() == MWMechanics::AiPackage::TypeIdPursue) + { + aiSeq.stopPursuit(); + aiSeq.stack(MWMechanics::AiCombat(target), ptr); + } + } + } + } + } // Must be done after the target is set up, so that CreatureTargetted dialogue filter works properly - if (ptr.getClass().isNpc()) + if (ptr.getClass().isNpc() && !ptr.getClass().getCreatureStats(ptr).isDead()) MWBase::Environment::get().getDialogueManager()->say(ptr, "attack"); } @@ -1047,4 +1281,80 @@ namespace MWMechanics std::list MechanicsManager::getActorsFighting(const MWWorld::Ptr& actor) { return mActors.getActorsFighting(actor); } + + int MechanicsManager::countSavedGameRecords() const + { + return 1; // Death counter + } + + void MechanicsManager::write(ESM::ESMWriter &writer, Loading::Listener &listener) const + { + mActors.write(writer, listener); + } + + void MechanicsManager::readRecord(ESM::ESMReader &reader, int32_t type) + { + mActors.readRecord(reader, type); + } + + void MechanicsManager::clear() + { + mActors.clear(); + } + + bool MechanicsManager::isAggressive(const MWWorld::Ptr &ptr, const MWWorld::Ptr &target, int bias, bool ignoreDistance) + { + Ogre::Vector3 pos1 (ptr.getRefData().getPosition().pos); + Ogre::Vector3 pos2 (target.getRefData().getPosition().pos); + + float d = 0; + if (!ignoreDistance) + d = pos1.distance(pos2); + + static int iFightDistanceBase = MWBase::Environment::get().getWorld()->getStore().get().find( + "iFightDistanceBase")->getInt(); + static float fFightDistanceMultiplier = MWBase::Environment::get().getWorld()->getStore().get().find( + "fFightDistanceMultiplier")->getFloat(); + static float fFightDispMult = MWBase::Environment::get().getWorld()->getStore().get().find( + "fFightDispMult")->getFloat(); + + int disposition = 50; + if (ptr.getClass().isNpc()) + disposition = getDerivedDisposition(ptr); + + int fight = std::max(0.f, ptr.getClass().getCreatureStats(ptr).getAiSetting(CreatureStats::AI_Fight).getModified() + + (iFightDistanceBase - fFightDistanceMultiplier * d) + + ((50 - disposition) * fFightDispMult)) + + bias; + + if (ptr.getClass().isNpc() && target.getClass().isNpc()) + { + if (target.getClass().getNpcStats(target).isWerewolf() || + (target == MWBase::Environment::get().getWorld()->getPlayerPtr() && + MWBase::Environment::get().getWorld()->getGlobalInt("pcknownwerewolf"))) + { + const ESM::GameSetting * iWerewolfFightMod = MWBase::Environment::get().getWorld()->getStore().get().search("iWerewolfFightMod"); + fight += iWerewolfFightMod->getInt(); + } + } + + return (fight >= 100); + } + + void MechanicsManager::keepPlayerAlive() + { + MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); + CreatureStats& stats = player.getClass().getCreatureStats(player); + if (stats.isDead()) + { + MWMechanics::DynamicStat stat (stats.getHealth()); + + if (stat.getModified()<1) + { + stat.setModified(1, 0); + stats.setHealth(stat); + } + stats.resurrect(); + } + } } diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp index dcd12ee14..9f9e85c5a 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp @@ -26,6 +26,7 @@ namespace MWMechanics { MWWorld::Ptr mWatched; NpcStats mWatchedStats; + bool mWatchedStatsEmpty; bool mUpdatePlayer; bool mClassSelected; bool mRaceSelected; @@ -111,6 +112,7 @@ namespace MWMechanics /** * @brief Commit a crime. If any actors witness the crime and report it, * reportCrime will be called automatically. + * @note victim may be empty * @param arg Depends on \a type, e.g. for Theft, the value of the item that was stolen. * @return was the crime reported? */ @@ -118,6 +120,8 @@ namespace MWMechanics OffenseType type, int arg=0); virtual void reportCrime (const MWWorld::Ptr& ptr, const MWWorld::Ptr& victim, OffenseType type, int arg=0); + /// @return false if the attack was considered a "friendly hit" and forgiven + virtual bool actorAttacked (const MWWorld::Ptr& victim, const MWWorld::Ptr& attacker); /// Utility to check if taking this item is illegal and calling commitCrime if so virtual void itemTaken (const MWWorld::Ptr& ptr, const MWWorld::Ptr& item, int count); /// Utility to check if opening (i.e. unlocking) this object is illegal and calling commitCrime if so @@ -126,6 +130,9 @@ namespace MWMechanics /// @return was it illegal, and someone saw you doing it? Also returns fail when enemies are nearby virtual bool sleepInBed (const MWWorld::Ptr& ptr, const MWWorld::Ptr& bed); + /// @return is \a ptr allowed to take/use \a item or is it a crime? + virtual bool isAllowedToUse (const MWWorld::Ptr& ptr, const MWWorld::Ptr& item, MWWorld::Ptr& victim); + virtual void forceStateUpdate(const MWWorld::Ptr &ptr); virtual void playAnimationGroup(const MWWorld::Ptr& ptr, const std::string& groupName, int mode, int number); @@ -147,6 +154,20 @@ namespace MWMechanics virtual bool isAIActive(); virtual void playerLoaded(); + + virtual int countSavedGameRecords() const; + + virtual void write (ESM::ESMWriter& writer, Loading::Listener& listener) const; + + virtual void readRecord (ESM::ESMReader& reader, int32_t type); + + virtual void clear(); + + /// @param bias Can be used to add an additional aggression bias towards the target, + /// making it more likely for the function to return true. + virtual bool isAggressive (const MWWorld::Ptr& ptr, const MWWorld::Ptr& target, int bias=0, bool ignoreDistance=false); + + virtual void keepPlayerAlive(); }; } diff --git a/apps/openmw/mwmechanics/npcstats.cpp b/apps/openmw/mwmechanics/npcstats.cpp index d50f2c5ae..13fc14318 100644 --- a/apps/openmw/mwmechanics/npcstats.cpp +++ b/apps/openmw/mwmechanics/npcstats.cpp @@ -34,7 +34,6 @@ MWMechanics::NpcStats::NpcStats() , mProfit(0) , mTimeToStartDrowning(20.0) , mLastDrowningHit(0) -, mLevelHealthBonus(0) { mSkillIncreases.resize (ESM::Attribute::Length, 0); } @@ -70,9 +69,41 @@ const std::map& MWMechanics::NpcStats::getFactionRanks() const return mFactionRank; } -std::map& MWMechanics::NpcStats::getFactionRanks() +void MWMechanics::NpcStats::raiseRank(const std::string &faction) { - return mFactionRank; + const std::string lower = Misc::StringUtils::lowerCase(faction); + std::map::iterator it = mFactionRank.find(lower); + if (it != mFactionRank.end()) + { + // Does the next rank exist? + const ESM::Faction* faction = MWBase::Environment::get().getWorld()->getStore().get().find(lower); + if (it->second+1 < 10 && !faction->mRanks[it->second+1].empty()) + it->second += 1; + } +} + +void MWMechanics::NpcStats::lowerRank(const std::string &faction) +{ + const std::string lower = Misc::StringUtils::lowerCase(faction); + std::map::iterator it = mFactionRank.find(lower); + if (it != mFactionRank.end()) + { + it->second = std::max(0, it->second-1); + } +} + +void MWMechanics::NpcStats::setFactionRank(const std::string &faction, int rank) +{ + const std::string lower = Misc::StringUtils::lowerCase(faction); + mFactionRank[lower] = rank; +} + +void MWMechanics::NpcStats::joinFaction(const std::string& faction) +{ + const std::string lower = Misc::StringUtils::lowerCase(faction); + std::map::iterator it = mFactionRank.find(lower); + if (it == mFactionRank.end()) + mFactionRank[lower] = 0; } bool MWMechanics::NpcStats::getExpelled(const std::string& factionID) const @@ -108,7 +139,7 @@ bool MWMechanics::NpcStats::isSameFaction (const NpcStats& npcStats) const } float MWMechanics::NpcStats::getSkillGain (int skillIndex, const ESM::Class& class_, int usageType, - int level) const + int level, float extraFactor) const { if (level<0) level = static_cast (getSkill (skillIndex).getBase()); @@ -132,6 +163,8 @@ float MWMechanics::NpcStats::getSkillGain (int skillIndex, const ESM::Class& cla return 0; } + skillFactor *= extraFactor; + const MWWorld::Store &gmst = MWBase::Environment::get().getWorld()->getStore().get(); @@ -168,7 +201,7 @@ float MWMechanics::NpcStats::getSkillGain (int skillIndex, const ESM::Class& cla return 1.0 / ((level+1) * (1.0/skillFactor) * typeFactor * specialisationFactor); } -void MWMechanics::NpcStats::useSkill (int skillIndex, const ESM::Class& class_, int usageType) +void MWMechanics::NpcStats::useSkill (int skillIndex, const ESM::Class& class_, int usageType, float extraFactor) { // Don't increase skills as a werewolf if(mIsWerewolf) @@ -176,7 +209,7 @@ void MWMechanics::NpcStats::useSkill (int skillIndex, const ESM::Class& class_, MWMechanics::SkillValue& value = getSkill (skillIndex); - value.setProgress(value.getProgress() + getSkillGain (skillIndex, class_, usageType)); + value.setProgress(value.getProgress() + getSkillGain (skillIndex, class_, usageType, -1, extraFactor)); if (value.getProgress()>=1) { @@ -262,8 +295,7 @@ void MWMechanics::NpcStats::levelUp() // "When you gain a level, in addition to increasing three primary attributes, your Health // will automatically increase by 10% of your Endurance attribute. If you increased Endurance this level, // the Health increase is calculated from the increased Endurance" - mLevelHealthBonus += endurance * gmst.find("fLevelUpHealthEndMult")->getFloat(); - updateHealth(); + setHealth(getHealth().getBase() + endurance * gmst.find("fLevelUpHealthEndMult")->getFloat()); setLevel(getLevel()+1); } @@ -273,7 +305,7 @@ void MWMechanics::NpcStats::updateHealth() const int endurance = getAttribute(ESM::Attribute::Endurance).getBase(); const int strength = getAttribute(ESM::Attribute::Strength).getBase(); - setHealth(static_cast (0.5 * (strength + endurance)) + mLevelHealthBonus); + setHealth(static_cast (0.5 * (strength + endurance))); } int MWMechanics::NpcStats::getLevelupAttributeMultiplier(int attribute) const @@ -361,8 +393,14 @@ bool MWMechanics::NpcStats::hasSkillsForRank (const std::string& factionId, int std::vector skills; - for (int i=0; i<6; ++i) - skills.push_back (static_cast (getSkill (faction.mData.mSkills[i]).getModified())); + for (int i=0; i<7; ++i) + { + if (faction.mData.mSkills[i] != -1) + skills.push_back (static_cast (getSkill (faction.mData.mSkills[i]).getModified())); + } + + if (skills.empty()) + return true; std::sort (skills.begin(), skills.end()); @@ -373,6 +411,9 @@ bool MWMechanics::NpcStats::hasSkillsForRank (const std::string& factionId, int if (*iter=rankData.mSkill2; } @@ -387,6 +428,8 @@ void MWMechanics::NpcStats::setWerewolf (bool set) { const MWWorld::Store &gmst = MWBase::Environment::get().getWorld()->getStore().get(); + mWerewolfKills = 0; + for(size_t i = 0;i < ESM::Attribute::Length;i++) { mWerewolfAttributes[i] = getAttribute(i); @@ -418,6 +461,11 @@ int MWMechanics::NpcStats::getWerewolfKills() const return mWerewolfKills; } +void MWMechanics::NpcStats::addWerewolfKill() +{ + ++mWerewolfKills; +} + int MWMechanics::NpcStats::getProfit() const { return mProfit; @@ -481,7 +529,6 @@ void MWMechanics::NpcStats::writeState (ESM::NpcStats& state) const state.mTimeToStartDrowning = mTimeToStartDrowning; state.mLastDrowningHit = mLastDrowningHit; - state.mLevelHealthBonus = mLevelHealthBonus; } void MWMechanics::NpcStats::readState (const ESM::NpcStats& state) @@ -533,5 +580,4 @@ void MWMechanics::NpcStats::readState (const ESM::NpcStats& state) mTimeToStartDrowning = state.mTimeToStartDrowning; mLastDrowningHit = state.mLastDrowningHit; - mLevelHealthBonus = state.mLevelHealthBonus; } diff --git a/apps/openmw/mwmechanics/npcstats.hpp b/apps/openmw/mwmechanics/npcstats.hpp index 185a58b94..ee897033b 100644 --- a/apps/openmw/mwmechanics/npcstats.hpp +++ b/apps/openmw/mwmechanics/npcstats.hpp @@ -19,9 +19,6 @@ namespace ESM namespace MWMechanics { /// \brief Additional stats for NPCs - /// - /// \note For technical reasons the spell list and the currently selected spell is also handled by - /// CreatureStats, even though they are actually NPC stats. class NpcStats : public CreatureStats { @@ -52,8 +49,6 @@ namespace MWMechanics /// time since last hit from drowning float mLastDrowningHit; - float mLevelHealthBonus; - public: NpcStats(); @@ -75,7 +70,15 @@ namespace MWMechanics SkillValue& getSkill (int index); const std::map& getFactionRanks() const; - std::map& getFactionRanks(); + /// Increase the rank in this faction by 1, if such a rank exists. + void raiseRank(const std::string& faction); + /// Lower the rank in this faction by 1, if such a rank exists. + void lowerRank(const std::string& faction); + /// Join this faction, setting the initial rank to 0. + void joinFaction(const std::string& faction); + /// Warning: this function performs no check whether the rank exists, + /// and should be used in initial actor setup only. + void setFactionRank(const std::string& faction, int rank); const std::set& getExpelled() const { return mExpelled; } bool getExpelled(const std::string& factionID) const; @@ -86,12 +89,12 @@ namespace MWMechanics ///< Do *this and \a npcStats share a faction? float getSkillGain (int skillIndex, const ESM::Class& class_, int usageType = -1, - int level = -1) const; + int level = -1, float extraFactor=1.f) const; ///< \param usageType: Usage specific factor, specified in the respective skill record; /// -1: use a factor of 1.0 instead. /// \param level Level to base calculation on; -1: use current level. - void useSkill (int skillIndex, const ESM::Class& class_, int usageType = -1); + void useSkill (int skillIndex, const ESM::Class& class_, int usageType = -1, float extraFactor=1.f); ///< Increase skill by usage. void increaseSkill (int skillIndex, const ESM::Class& class_, bool preserveProgress); @@ -104,7 +107,7 @@ namespace MWMechanics void updateHealth(); ///< Calculate health based on endurance and strength. - /// Called at character creation and at level up. + /// Called at character creation. void flagAsUsed (const std::string& id); @@ -126,6 +129,9 @@ namespace MWMechanics int getWerewolfKills() const; + /// Increments mWerewolfKills by 1. + void addWerewolfKill(); + float getTimeToStartDrowning() const; /// Sets time left for the creature to drown if it stays underwater. /// @param time value from [0,20] diff --git a/apps/openmw/mwmechanics/pathfinding.cpp b/apps/openmw/mwmechanics/pathfinding.cpp index d77a35ea4..f1279c415 100644 --- a/apps/openmw/mwmechanics/pathfinding.cpp +++ b/apps/openmw/mwmechanics/pathfinding.cpp @@ -90,12 +90,12 @@ namespace namespace MWMechanics { - float distanceZCorrected(ESM::Pathgrid::Point point, float x, float y, float z) + float sqrDistanceZCorrected(ESM::Pathgrid::Point point, float x, float y, float z) { x -= point.mX; y -= point.mY; z -= point.mZ; - return sqrt(x * x + y * y + 0.1 * z * z); + return (x * x + y * y + 0.1 * z * z); } float distance(ESM::Pathgrid::Point point, float x, float y, float z) @@ -278,16 +278,8 @@ namespace MWMechanics const ESM::Pathgrid::Point &nextPoint = *mPath.begin(); float directionX = nextPoint.mX - x; float directionY = nextPoint.mY - y; - float directionResult = sqrt(directionX * directionX + directionY * directionY); - return Ogre::Radian(Ogre::Math::ACos(directionY / directionResult) * sgn(Ogre::Math::ASin(directionX / directionResult))).valueDegrees(); - } - - // Used by AiCombat, use Euclidean distance - float PathFinder::getDistToNext(float x, float y, float z) - { - ESM::Pathgrid::Point nextPoint = *mPath.begin(); - return distance(nextPoint, x, y, z); + return Ogre::Math::ATan2(directionX,directionY).valueDegrees(); } bool PathFinder::checkWaypoint(float x, float y, float z) @@ -296,7 +288,7 @@ namespace MWMechanics return true; ESM::Pathgrid::Point nextPoint = *mPath.begin(); - if(distanceZCorrected(nextPoint, x, y, z) < 64) + if(sqrDistanceZCorrected(nextPoint, x, y, z) < 64*64) { mPath.pop_front(); if(mPath.empty()) mIsPathConstructed = false; @@ -311,7 +303,7 @@ namespace MWMechanics return true; ESM::Pathgrid::Point nextPoint = *mPath.begin(); - if(distanceZCorrected(nextPoint, x, y, z) < 64) + if(sqrDistanceZCorrected(nextPoint, x, y, z) < 64*64) { mPath.pop_front(); if(mPath.empty()) diff --git a/apps/openmw/mwmechanics/pathfinding.hpp b/apps/openmw/mwmechanics/pathfinding.hpp index 603a04f8c..482808dac 100644 --- a/apps/openmw/mwmechanics/pathfinding.hpp +++ b/apps/openmw/mwmechanics/pathfinding.hpp @@ -47,8 +47,6 @@ namespace MWMechanics float getZAngleToNext(float x, float y) const; - float getDistToNext(float x, float y, float z); - bool isPathConstructed() const { return mIsPathConstructed; diff --git a/apps/openmw/mwmechanics/pathgrid.cpp b/apps/openmw/mwmechanics/pathgrid.cpp index c3fa0a662..4983a4a4f 100644 --- a/apps/openmw/mwmechanics/pathgrid.cpp +++ b/apps/openmw/mwmechanics/pathgrid.cpp @@ -100,6 +100,9 @@ namespace MWMechanics if(!cell) return false; + if(mIsGraphConstructed) + return true; + mCell = cell; mIsExterior = cell->isExterior(); mPathgrid = MWBase::Environment::get().getWorld()->getStore().get().search(*cell); @@ -107,8 +110,6 @@ namespace MWMechanics if(!mPathgrid) return false; - if(mIsGraphConstructed) - return true; mGraph.resize(mPathgrid->mPoints.size()); for(int i = 0; i < static_cast (mPathgrid->mEdges.size()); i++) diff --git a/apps/openmw/mwmechanics/repair.cpp b/apps/openmw/mwmechanics/repair.cpp index 9f2c851cf..6d6f889ed 100644 --- a/apps/openmw/mwmechanics/repair.cpp +++ b/apps/openmw/mwmechanics/repair.cpp @@ -57,10 +57,13 @@ void Repair::repair(const MWWorld::Ptr &itemToRepair) charge = std::min(charge + y, itemToRepair.getClass().getItemMaxHealth(itemToRepair)); itemToRepair.getCellRef().setCharge(charge); + // attempt to re-stack item, in case it was fully repaired + MWWorld::ContainerStoreIterator stacked = player.getClass().getContainerStore(player).restack(itemToRepair); + // set the OnPCRepair variable on the item's script - std::string script = itemToRepair.getClass().getScript(itemToRepair); + std::string script = stacked->getClass().getScript(itemToRepair); if(script != "") - itemToRepair.getRefData().getLocals().setVarByInt(script, "onpcrepair", 1); + stacked->getRefData().getLocals().setVarByInt(script, "onpcrepair", 1); // increase skill player.getClass().skillUsageSucceeded(player, ESM::Skill::Armorer, 0); diff --git a/apps/openmw/mwmechanics/spellcasting.cpp b/apps/openmw/mwmechanics/spellcasting.cpp index c996e90d6..d33bb6ad1 100644 --- a/apps/openmw/mwmechanics/spellcasting.cpp +++ b/apps/openmw/mwmechanics/spellcasting.cpp @@ -72,11 +72,11 @@ namespace MWMechanics return schoolSkillMap[school]; } - float getSpellSuccessChance (const ESM::Spell* spell, const MWWorld::Ptr& actor, int* effectiveSchool) + float getSpellSuccessChance (const ESM::Spell* spell, const MWWorld::Ptr& actor, int* effectiveSchool, bool cap) { CreatureStats& stats = actor.getClass().getCreatureStats(actor); - if (stats.getMagicEffects().get(ESM::MagicEffect::Silence).mMagnitude) + if (stats.getMagicEffects().get(ESM::MagicEffect::Silence).getMagnitude()) return 0; float y = FLT_MAX; @@ -92,7 +92,7 @@ namespace MWMechanics x *= 0.1 * magicEffect->mData.mBaseCost; x *= 0.5 * (it->mMagnMin + it->mMagnMax); x *= it->mArea * 0.05 * magicEffect->mData.mBaseCost; - if (magicEffect->mData.mFlags & ESM::MagicEffect::CastTarget) + if (it->mRange == ESM::RT_Target) x *= 1.5; static const float fEffectCostMult = MWBase::Environment::get().getWorld()->getStore().get().find( "fEffectCostMult")->getFloat(); @@ -114,7 +114,7 @@ namespace MWMechanics if (spell->mData.mFlags & ESM::Spell::F_Always) return 100; - int castBonus = -stats.getMagicEffects().get(ESM::MagicEffect::Sound).mMagnitude; + int castBonus = -stats.getMagicEffects().get(ESM::MagicEffect::Sound).getMagnitude(); int actorWillpower = stats.getAttribute(ESM::Attribute::Willpower).getModified(); int actorLuck = stats.getAttribute(ESM::Attribute::Luck).getModified(); @@ -123,14 +123,17 @@ namespace MWMechanics if (MWBase::Environment::get().getWorld()->getGodModeState() && actor.getRefData().getHandle() == "player") castChance = 100; - return std::max(0.f, std::min(100.f, castChance)); + if (!cap) + return std::max(0.f, castChance); + else + return std::max(0.f, std::min(100.f, castChance)); } - float getSpellSuccessChance (const std::string& spellId, const MWWorld::Ptr& actor, int* effectiveSchool) + float getSpellSuccessChance (const std::string& spellId, const MWWorld::Ptr& actor, int* effectiveSchool, bool cap) { const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().find(spellId); - return getSpellSuccessChance(spell, actor, effectiveSchool); + return getSpellSuccessChance(spell, actor, effectiveSchool, cap); } @@ -148,39 +151,74 @@ namespace MWMechanics return school; } - float getEffectResistance (short effectId, const MWWorld::Ptr& actor, const MWWorld::Ptr& caster, const ESM::Spell* spell) + bool spellIncreasesSkill(const ESM::Spell *spell) + { + if (spell->mData.mType == ESM::Spell::ST_Spell && !(spell->mData.mFlags & ESM::Spell::F_Always)) + return true; + return false; + } + + bool spellIncreasesSkill(const std::string &spellId) + { + const ESM::Spell* spell = + MWBase::Environment::get().getWorld()->getStore().get().find(spellId); + return spellIncreasesSkill(spell); + } + + float getEffectResistanceAttribute (short effectId, const MagicEffects* actorEffects) + { + short resistanceEffect = ESM::MagicEffect::getResistanceEffect(effectId); + short weaknessEffect = ESM::MagicEffect::getWeaknessEffect(effectId); + + float resistance = 0; + if (resistanceEffect != -1) + resistance += actorEffects->get(resistanceEffect).getMagnitude(); + if (weaknessEffect != -1) + resistance -= actorEffects->get(weaknessEffect).getMagnitude(); + + if (effectId == ESM::MagicEffect::FireDamage) + resistance += actorEffects->get(ESM::MagicEffect::FireShield).getMagnitude(); + if (effectId == ESM::MagicEffect::ShockDamage) + resistance += actorEffects->get(ESM::MagicEffect::LightningShield).getMagnitude(); + if (effectId == ESM::MagicEffect::FrostDamage) + resistance += actorEffects->get(ESM::MagicEffect::FrostShield).getMagnitude(); + + return resistance; + } + + float getEffectResistance (short effectId, const MWWorld::Ptr& actor, const MWWorld::Ptr& caster, + const ESM::Spell* spell, const MagicEffects* effects) { const ESM::MagicEffect *magicEffect = MWBase::Environment::get().getWorld()->getStore().get().find ( effectId); const MWMechanics::CreatureStats& stats = actor.getClass().getCreatureStats(actor); + const MWMechanics::MagicEffects* magicEffects = &stats.getMagicEffects(); + if (effects) + magicEffects = effects; float resisted = 0; if (magicEffect->mData.mFlags & ESM::MagicEffect::Harmful) { + // Effects with no resistance attribute belonging to them can not be resisted + if (ESM::MagicEffect::getResistanceEffect(effectId) == -1) + return 0.f; - short resistanceEffect = ESM::MagicEffect::getResistanceEffect(effectId); - short weaknessEffect = ESM::MagicEffect::getWeaknessEffect(effectId); - - float resistance = 0; - if (resistanceEffect != -1) - resistance += stats.getMagicEffects().get(resistanceEffect).mMagnitude; - if (weaknessEffect != -1) - resistance -= stats.getMagicEffects().get(weaknessEffect).mMagnitude; - + float resistance = getEffectResistanceAttribute(effectId, magicEffects); float willpower = stats.getAttribute(ESM::Attribute::Willpower).getModified(); float luck = stats.getAttribute(ESM::Attribute::Luck).getModified(); float x = (willpower + 0.1 * luck) * stats.getFatigueTerm(); // This makes spells that are easy to cast harder to resist and vice versa + float castChance = 100.f; if (spell != NULL && !caster.isEmpty() && caster.getClass().isActor()) { - float castChance = getSpellSuccessChance(spell, caster); - if (castChance > 0) - x *= 50 / castChance; + castChance = getSpellSuccessChance(spell, caster, NULL, false); // Uncapped casting chance } + if (castChance > 0) + x *= 50 / castChance; float roll = static_cast(std::rand()) / RAND_MAX * 100; if (magicEffect->mData.mFlags & ESM::MagicEffect::NoMagnitude) @@ -204,13 +242,49 @@ namespace MWMechanics return resisted; } - float getEffectMultiplier(short effectId, const MWWorld::Ptr& actor, const MWWorld::Ptr& caster, const ESM::Spell* spell) + float getEffectMultiplier(short effectId, const MWWorld::Ptr& actor, const MWWorld::Ptr& caster, + const ESM::Spell* spell, const MagicEffects* effects) { - float resistance = getEffectResistance(effectId, actor, caster, spell); - if (resistance >= 0) - return 1 - resistance / 100.f; - else - return -(resistance-100) / 100.f; + float resistance = getEffectResistance(effectId, actor, caster, spell, effects); + return 1 - resistance / 100.f; + } + + /// Check if the given affect can be applied to the target. If \a castByPlayer, emits a message box on failure. + bool checkEffectTarget (int effectId, const MWWorld::Ptr& target, bool castByPlayer) + { + switch (effectId) + { + case ESM::MagicEffect::Levitate: + if (!MWBase::Environment::get().getWorld()->isLevitationEnabled()) + { + if (castByPlayer) + MWBase::Environment::get().getWindowManager()->messageBox("#{sLevitateDisabled}"); + return false; + } + break; + case ESM::MagicEffect::Soultrap: + if (!target.getClass().isNpc() // no messagebox for NPCs + && (target.getTypeName() == typeid(ESM::Creature).name() && target.get()->mBase->mData.mSoul == 0)) + { + if (castByPlayer) + MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicInvalidTarget}"); + return true; // must still apply to get visual effect and have target regard it as attack + } + break; + case ESM::MagicEffect::AlmsiviIntervention: + case ESM::MagicEffect::DivineIntervention: + case ESM::MagicEffect::Mark: + case ESM::MagicEffect::Recall: + if (!MWBase::Environment::get().getWorld()->isTeleportingEnabled()) + { + if (castByPlayer) + MWBase::Environment::get().getWindowManager()->messageBox("#{sTeleportDisabled}"); + return false; + } + break; + } + + return true; } CastSpell::CastSpell(const MWWorld::Ptr &caster, const MWWorld::Ptr &target) @@ -218,6 +292,7 @@ namespace MWMechanics , mTarget(target) , mStack(false) , mHitPosition(0,0,0) + , mAlwaysSucceed(false) { } @@ -243,8 +318,8 @@ namespace MWMechanics if (spell && (spell->mData.mType == ESM::Spell::ST_Disease || spell->mData.mType == ESM::Spell::ST_Blight)) { float x = (spell->mData.mType == ESM::Spell::ST_Disease) ? - target.getClass().getCreatureStats(target).getMagicEffects().get(ESM::MagicEffect::ResistCommonDisease).mMagnitude - : target.getClass().getCreatureStats(target).getMagicEffects().get(ESM::MagicEffect::ResistBlightDisease).mMagnitude; + target.getClass().getCreatureStats(target).getMagicEffects().get(ESM::MagicEffect::ResistCommonDisease).getMagnitude() + : target.getClass().getCreatureStats(target).getMagicEffects().get(ESM::MagicEffect::ResistBlightDisease).getMagnitude(); int roll = std::rand()/ (static_cast (RAND_MAX) + 1) * 100; // [0, 99] if (roll <= x) @@ -261,8 +336,36 @@ namespace MWMechanics bool firstAppliedEffect = true; bool anyHarmfulEffect = false; + // HACK: cache target's magic effects here, and add any applied effects to it. Use the cached effects for determining resistance. + // This is required for Weakness effects in a spell to apply to any subsequent effects in the spell. + // Otherwise, they'd only apply after the whole spell was added. + MagicEffects targetEffects; + if (target.getClass().isActor()) + targetEffects += target.getClass().getCreatureStats(target).getMagicEffects(); + bool castByPlayer = (!caster.isEmpty() && caster.getRefData().getHandle() == "player"); + // Try absorbing if it's a spell + // NOTE: Vanilla does this once per effect source instead of adding the % from all sources together, not sure + // if that is worth replicating. + bool absorbed = false; + if (spell && caster != target && target.getClass().isActor()) + { + int absorb = target.getClass().getCreatureStats(target).getMagicEffects().get(ESM::MagicEffect::SpellAbsorption).getMagnitude(); + int roll = std::rand()/ (static_cast (RAND_MAX) + 1) * 100; // [0, 99] + absorbed = (roll < absorb); + if (absorbed) + { + const ESM::Static* absorbStatic = MWBase::Environment::get().getWorld()->getStore().get().find ("VFX_Absorb"); + MWBase::Environment::get().getWorld()->getAnimation(target)->addEffect( + "meshes\\" + absorbStatic->mModel, ESM::MagicEffect::SpellAbsorption, false, ""); + // Magicka is increased by cost of spell + DynamicStat magicka = target.getClass().getCreatureStats(target).getMagicka(); + magicka.setCurrent(magicka.getCurrent() + spell->mData.mCost); + target.getClass().getCreatureStats(target).setMagicka(magicka); + } + } + for (std::vector::const_iterator effectIt (effects.mList.begin()); effectIt!=effects.mList.end(); ++effectIt) { @@ -273,23 +376,8 @@ namespace MWMechanics MWBase::Environment::get().getWorld()->getStore().get().find ( effectIt->mEffectID); - if (!MWBase::Environment::get().getWorld()->isLevitationEnabled() && effectIt->mEffectID == ESM::MagicEffect::Levitate) - { - if (castByPlayer) - MWBase::Environment::get().getWindowManager()->messageBox("#{sLevitateDisabled}"); + if (!checkEffectTarget(effectIt->mEffectID, target, castByPlayer)) continue; - } - - if (!MWBase::Environment::get().getWorld()->isTeleportingEnabled() && - (effectIt->mEffectID == ESM::MagicEffect::AlmsiviIntervention || - effectIt->mEffectID == ESM::MagicEffect::DivineIntervention || - effectIt->mEffectID == ESM::MagicEffect::Mark || - effectIt->mEffectID == ESM::MagicEffect::Recall)) - { - if (castByPlayer) - MWBase::Environment::get().getWindowManager()->messageBox("#{sTeleportDisabled}"); - continue; - } // If player is healing someone, show the target's HP bar if (castByPlayer && target != caster @@ -302,35 +390,17 @@ namespace MWMechanics { anyHarmfulEffect = true; + if (absorbed) // Absorbed, and we know there was a harmful effect (figuring that out is the only reason we are in this loop) + break; + // If player is attempting to cast a harmful spell, show the target's HP bar if (castByPlayer && target != caster) MWBase::Environment::get().getWindowManager()->setEnemy(target); - // Try absorbing if it's a spell - // NOTE: Vanilla does this once per effect source instead of adding the % from all sources together, not sure - // if that is worth replicating. - if (spell && caster != target) - { - int absorb = target.getClass().getCreatureStats(target).getMagicEffects().get(ESM::MagicEffect::SpellAbsorption).mMagnitude; - int roll = std::rand()/ (static_cast (RAND_MAX) + 1) * 100; // [0, 99] - bool isAbsorbed = (roll < absorb); - if (isAbsorbed) - { - const ESM::Static* absorbStatic = MWBase::Environment::get().getWorld()->getStore().get().find ("VFX_Absorb"); - MWBase::Environment::get().getWorld()->getAnimation(target)->addEffect( - "meshes\\" + absorbStatic->mModel, ESM::MagicEffect::Reflect, false, ""); - // Magicka is increased by cost of spell - DynamicStat magicka = target.getClass().getCreatureStats(target).getMagicka(); - magicka.setCurrent(magicka.getCurrent() + spell->mData.mCost); - target.getClass().getCreatureStats(target).setMagicka(magicka); - magnitudeMult = 0; - } - } - // Try reflecting - if (!reflected && magnitudeMult > 0 && caster != target && !(magicEffect->mData.mFlags & ESM::MagicEffect::Unreflectable)) + if (!reflected && magnitudeMult > 0 && !caster.isEmpty() && caster != target && !(magicEffect->mData.mFlags & ESM::MagicEffect::Unreflectable)) { - int reflect = target.getClass().getCreatureStats(target).getMagicEffects().get(ESM::MagicEffect::Reflect).mMagnitude; + int reflect = target.getClass().getCreatureStats(target).getMagicEffects().get(ESM::MagicEffect::Reflect).getMagnitude(); int roll = std::rand()/ (static_cast (RAND_MAX) + 1) * 100; // [0, 99] bool isReflected = (roll < reflect); if (isReflected) @@ -346,21 +416,19 @@ namespace MWMechanics // Try resisting if (magnitudeMult > 0 && target.getClass().isActor()) { - - magnitudeMult = MWMechanics::getEffectMultiplier(effectIt->mEffectID, target, caster, spell); + magnitudeMult = MWMechanics::getEffectMultiplier(effectIt->mEffectID, target, caster, spell, &targetEffects); if (magnitudeMult == 0) { // Fully resisted, show message if (target.getRefData().getHandle() == "player") MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicPCResisted}"); - else + else if (castByPlayer) MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicTargetResisted}"); } } } - - if (magnitudeMult > 0) + if (magnitudeMult > 0 && !absorbed) { float random = std::rand() / static_cast(RAND_MAX); float magnitude = effectIt->mMagnMin + (effectIt->mMagnMax - effectIt->mMagnMin) * random; @@ -375,6 +443,8 @@ namespace MWMechanics effect.mDuration = effectIt->mDuration; effect.mMagnitude = magnitude; + targetEffects.add(MWMechanics::EffectKey(*effectIt), MWMechanics::EffectParam(effect.mMagnitude)); + appliedLastingEffects.push_back(effect); // For absorb effects, also apply the effect to the caster - but with a negative @@ -447,19 +517,19 @@ namespace MWMechanics MWBase::Environment::get().getWorld()->explodeSpell(mHitPosition, effects, caster, mId, mSourceName); if (!reflectedEffects.mList.empty()) - inflict(caster, target, reflectedEffects, range, true); + inflict(caster, target, reflectedEffects, range, true, exploded); if (!appliedLastingEffects.empty()) { int casterActorId = -1; - if (caster.getClass().isActor()) + if (!caster.isEmpty() && caster.getClass().isActor()) casterActorId = caster.getClass().getCreatureStats(caster).getActorId(); target.getClass().getCreatureStats(target).getActiveSpells().addSpell(mId, mStack, appliedLastingEffects, mSourceName, casterActorId); } // Notify the target actor they've been hit - if (anyHarmfulEffect && target.getClass().isActor() && target != caster && caster.getClass().isActor()) + if (anyHarmfulEffect && target.getClass().isActor() && target != caster && !caster.isEmpty() && caster.getClass().isActor()) target.getClass().onHit(target, 0.f, true, MWWorld::Ptr(), caster, true); } @@ -471,7 +541,11 @@ namespace MWMechanics if (effectId == ESM::MagicEffect::Lock) { if (target.getCellRef().getLockLevel() < magnitude) //If the door is not already locked to a higher value, lock it to spell magnitude + { + if (caster.getRefData().getHandle() == "player") + MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicLockSuccess}"); target.getCellRef().setLockLevel(magnitude); + } } else if (effectId == ESM::MagicEffect::Open) { @@ -479,12 +553,14 @@ namespace MWMechanics { if (target.getCellRef().getLockLevel() > 0) { - //Door not already unlocked MWBase::Environment::get().getSoundManager()->playSound3D(target, "Open Lock", 1.f, 1.f); if (!caster.isEmpty() && caster.getClass().isActor()) MWBase::Environment::get().getMechanicsManager()->objectOpened(caster, target); + + if (caster.getRefData().getHandle() == "player") + MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicOpenSuccess}"); } - target.getCellRef().setLockLevel(-abs(target.getCellRef().getLockLevel())); //unlocks the door + target.getCellRef().setLockLevel(-abs(target.getCellRef().getLockLevel())); } else MWBase::Environment::get().getSoundManager()->playSound3D(target, "Open Lock Fail", 1.f, 1.f); @@ -606,9 +682,23 @@ namespace MWMechanics if (item.getCellRef().getEnchantmentCharge() < castCost) { - // TODO: Should there be a sound here? if (mCaster.getRefData().getHandle() == "player") MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicInsufficientCharge}"); + + // Failure sound + int school = 0; + for (std::vector::const_iterator effectIt (enchantment->mEffects.mList.begin()); + effectIt!=enchantment->mEffects.mList.end(); ++effectIt) + { + const ESM::MagicEffect* magicEffect = MWBase::Environment::get().getWorld()->getStore().get().find(effectIt->mEffectID); + school = magicEffect->mData.mSchool; + break; + } + static const std::string schools[] = { + "alteration", "conjuration", "destruction", "illusion", "mysticism", "restoration" + }; + MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); + sndMgr->playSound3D(mCaster, "Spell Failure " + schools[school], 1.0f, 1.0f); return false; } // Reduce charge @@ -644,7 +734,9 @@ namespace MWMechanics getProjectileInfo(enchantment->mEffects, projectileModel, sound, speed); if (!projectileModel.empty()) MWBase::Environment::get().getWorld()->launchMagicBolt(projectileModel, sound, mId, speed, - false, enchantment->mEffects, mCaster, mSourceName); + false, enchantment->mEffects, mCaster, mSourceName, + // Not needed, enchantments can only be cast by actors + Ogre::Vector3(1,0,0)); return true; } @@ -670,7 +762,7 @@ namespace MWMechanics int school = 0; - if (mCaster.getClass().isActor()) + if (mCaster.getClass().isActor() && !mAlwaysSucceed) { school = getSpellSchool(spell, mCaster); @@ -680,7 +772,7 @@ namespace MWMechanics static const float fFatigueSpellBase = store.get().find("fFatigueSpellBase")->getFloat(); static const float fFatigueSpellMult = store.get().find("fFatigueSpellMult")->getFloat(); DynamicStat fatigue = stats.getFatigue(); - const float normalizedEncumbrance = mCaster.getClass().getEncumbrance(mCaster) / mCaster.getClass().getCapacity(mCaster); + const float normalizedEncumbrance = mCaster.getClass().getNormalizedEncumbrance(mCaster); float fatigueLoss = spell->mData.mCost * (fFatigueSpellBase + normalizedEncumbrance * fFatigueSpellMult); fatigue.setCurrent(fatigue.getCurrent() - fatigueLoss); stats.setFatigue(fatigue); @@ -709,7 +801,7 @@ namespace MWMechanics } } - if (mCaster.getRefData().getHandle() == "player" && spell->mData.mType == ESM::Spell::ST_Spell) + if (mCaster.getRefData().getHandle() == "player" && spellIncreasesSkill(spell)) mCaster.getClass().skillUsageSucceeded(mCaster, spellSchoolToSkill(school), 0); @@ -729,8 +821,18 @@ namespace MWMechanics float speed = 0; getProjectileInfo(spell->mEffects, projectileModel, sound, speed); if (!projectileModel.empty()) + { + Ogre::Vector3 fallbackDirection (0,1,0); + // Fall back to a "caster to target" direction if we have no other means of determining it + // (e.g. when cast by a non-actor) + if (!mTarget.isEmpty()) + fallbackDirection = + Ogre::Vector3(mTarget.getRefData().getPosition().pos)- + Ogre::Vector3(mCaster.getRefData().getPosition().pos); + MWBase::Environment::get().getWorld()->launchMagicBolt(projectileModel, sound, mId, speed, - false, spell->mEffects, mCaster, mSourceName); + false, spell->mEffects, mCaster, mSourceName, fallbackDirection); + } return true; } @@ -798,5 +900,4 @@ namespace MWMechanics return true; } - } diff --git a/apps/openmw/mwmechanics/spellcasting.hpp b/apps/openmw/mwmechanics/spellcasting.hpp index dce4b792e..395ae043b 100644 --- a/apps/openmw/mwmechanics/spellcasting.hpp +++ b/apps/openmw/mwmechanics/spellcasting.hpp @@ -18,6 +18,7 @@ namespace ESM namespace MWMechanics { class EffectKey; + class MagicEffects; ESM::Skill::SkillEnum spellSchoolToSkill(int school); @@ -25,37 +26,61 @@ namespace MWMechanics * @param spell spell to cast * @param actor calculate spell success chance for this actor (depends on actor's skills) * @param effectiveSchool the spell's effective school (relevant for skill progress) will be written here - * @attention actor has to be an NPC and not a creature! - * @return success chance from 0 to 100 (in percent) + * @param cap cap the result to 100%? + * @note actor can be an NPC or a creature + * @return success chance from 0 to 100 (in percent), if cap=false then chance above 100 may be returned. */ - float getSpellSuccessChance (const ESM::Spell* spell, const MWWorld::Ptr& actor, int* effectiveSchool = NULL); - float getSpellSuccessChance (const std::string& spellId, const MWWorld::Ptr& actor, int* effectiveSchool = NULL); + float getSpellSuccessChance (const ESM::Spell* spell, const MWWorld::Ptr& actor, int* effectiveSchool = NULL, bool cap=true); + float getSpellSuccessChance (const std::string& spellId, const MWWorld::Ptr& actor, int* effectiveSchool = NULL, bool cap=true); int getSpellSchool(const std::string& spellId, const MWWorld::Ptr& actor); int getSpellSchool(const ESM::Spell* spell, const MWWorld::Ptr& actor); - /// @return >=100 for fully resisted. can also return negative value for damage amplification. - float getEffectResistance (short effectId, const MWWorld::Ptr& actor, const MWWorld::Ptr& caster, const ESM::Spell* spell = NULL); + /// Get whether or not the given spell contributes to skill progress. + bool spellIncreasesSkill(const ESM::Spell* spell); + bool spellIncreasesSkill(const std::string& spellId); + + /// Get the resistance attribute against an effect for a given actor. This will add together + /// ResistX and Weakness to X effects relevant against the given effect. + float getEffectResistanceAttribute (short effectId, const MagicEffects* actorEffects); - float getEffectMultiplier(short effectId, const MWWorld::Ptr& actor, const MWWorld::Ptr& caster, const ESM::Spell* spell = NULL); + /// Get the effective resistance against an effect casted by the given actor in the given spell (optional). + /// @return >=100 for fully resisted. can also return negative value for damage amplification. + /// @param effects Override the actor's current magicEffects. Useful if there are effects currently + /// being applied (but not applied yet) that should also be considered. + float getEffectResistance (short effectId, const MWWorld::Ptr& actor, const MWWorld::Ptr& caster, + const ESM::Spell* spell = NULL, const MagicEffects* effects = NULL); + + /// Get an effect multiplier for applying an effect cast by the given actor in the given spell (optional). + /// @return effect multiplier from 0 to 2. (100% net resistance to 100% net weakness) + /// @param effects Override the actor's current magicEffects. Useful if there are effects currently + /// being applied (but not applied yet) that should also be considered. + float getEffectMultiplier(short effectId, const MWWorld::Ptr& actor, const MWWorld::Ptr& caster, + const ESM::Spell* spell = NULL, const MagicEffects* effects = NULL); class CastSpell { private: - MWWorld::Ptr mCaster; - MWWorld::Ptr mTarget; + MWWorld::Ptr mCaster; // May be empty + MWWorld::Ptr mTarget; // May be empty public: bool mStack; std::string mId; // ID of spell, potion, item etc std::string mSourceName; // Display name for spell, potion, etc Ogre::Vector3 mHitPosition; // Used for spawning area orb + bool mAlwaysSucceed; // Always succeed spells casted by NPCs/creatures regardless of their chance (default: false) public: CastSpell(const MWWorld::Ptr& caster, const MWWorld::Ptr& target); bool cast (const ESM::Spell* spell); + + /// @note mCaster must be an actor bool cast (const MWWorld::Ptr& item); + + /// @note mCaster must be an NPC bool cast (const ESM::Ingredient* ingredient); + bool cast (const ESM::Potion* potion); /// @note Auto detects if spell, ingredient or potion diff --git a/apps/openmw/mwmechanics/spells.cpp b/apps/openmw/mwmechanics/spells.cpp index dee1a1b05..a1b73bc47 100644 --- a/apps/openmw/mwmechanics/spells.cpp +++ b/apps/openmw/mwmechanics/spells.cpp @@ -44,6 +44,15 @@ namespace MWMechanics } } + if (hasCorprusEffect(spell)) + { + CorprusStats corprus; + corprus.mWorsenings = 0; + corprus.mNextWorsening = MWBase::Environment::get().getWorld()->getTimeStamp() + CorprusStats::sWorseningPeriod; + + mCorprusSpells[spellId] = corprus; + } + mSpells.insert (std::make_pair (Misc::StringUtils::lowerCase(spellId), random)); } } @@ -52,6 +61,26 @@ namespace MWMechanics { std::string lower = Misc::StringUtils::lowerCase(spellId); TContainer::iterator iter = mSpells.find (lower); + std::map::iterator corprusIt = mCorprusSpells.find(lower); + + // if it's corprus, remove negative and keep positive effects + if (corprusIt != mCorprusSpells.end()) + { + worsenCorprus(lower); + if (mPermanentSpellEffects.find(lower) != mPermanentSpellEffects.end()) + { + MagicEffects & effects = mPermanentSpellEffects[lower]; + for (MagicEffects::Collection::const_iterator effectIt = effects.begin(); effectIt != effects.end();) + { + const ESM::MagicEffect * magicEffect = MWBase::Environment::get().getWorld()->getStore().get().find(effectIt->first.mId); + if (magicEffect->mData.mFlags & ESM::MagicEffect::Harmful) + effects.remove((effectIt++)->first); + else + ++effectIt; + } + } + mCorprusSpells.erase(corprusIt); + } if (iter!=mSpells.end()) mSpells.erase (iter); @@ -87,6 +116,11 @@ namespace MWMechanics } } + for (std::map::const_iterator it = mPermanentSpellEffects.begin(); it != mPermanentSpellEffects.end(); ++it) + { + effects += it->second; + } + return effects; } @@ -154,7 +188,7 @@ namespace MWMechanics const ESM::Spell *spell = MWBase::Environment::get().getWorld()->getStore().get().find (iter->first); - if (spell->mData.mType == ESM::Spell::ST_Blight) + if (spell->mData.mType == ESM::Spell::ST_Blight && !hasCorprusEffect(spell)) mSpells.erase(iter++); else ++iter; @@ -168,7 +202,7 @@ namespace MWMechanics const ESM::Spell *spell = MWBase::Environment::get().getWorld()->getStore().get().find (iter->first); - if (Misc::StringUtils::ciEqual(spell->mId, "corprus")) + if (hasCorprusEffect(spell)) mSpells.erase(iter++); else ++iter; @@ -216,6 +250,48 @@ namespace MWMechanics } } + void Spells::worsenCorprus(const std::string &corpSpellId) + { + mCorprusSpells[corpSpellId].mNextWorsening = MWBase::Environment::get().getWorld()->getTimeStamp() + CorprusStats::sWorseningPeriod; + mCorprusSpells[corpSpellId].mWorsenings++; + + // update worsened effects + mPermanentSpellEffects[corpSpellId] = MagicEffects(); + const ESM::Spell * spell = MWBase::Environment::get().getWorld()->getStore().get().find(corpSpellId); + int i=0; + for (std::vector::const_iterator effectIt = spell->mEffects.mList.begin(); effectIt != spell->mEffects.mList.end(); ++effectIt, ++i) + { + const ESM::MagicEffect * magicEffect = MWBase::Environment::get().getWorld()->getStore().get().find(effectIt->mEffectID); + if ((effectIt->mEffectID != ESM::MagicEffect::Corprus) && (magicEffect->mData.mFlags & ESM::MagicEffect::UncappedDamage)) // APPLIED_ONCE + { + float random = 1.f; + if (mSpells[corpSpellId].find(i) != mSpells[corpSpellId].end()) + random = mSpells[corpSpellId].at(i); + + float magnitude = effectIt->mMagnMin + (effectIt->mMagnMax - effectIt->mMagnMin) * random; + magnitude *= std::max(1, mCorprusSpells[corpSpellId].mWorsenings); + mPermanentSpellEffects[corpSpellId].add(MWMechanics::EffectKey(*effectIt), MWMechanics::EffectParam(magnitude)); + } + } + } + + bool Spells::hasCorprusEffect(const ESM::Spell *spell) + { + for (std::vector::const_iterator effectIt = spell->mEffects.mList.begin(); effectIt != spell->mEffects.mList.end(); ++effectIt) + { + if (effectIt->mEffectID == ESM::MagicEffect::Corprus) + { + return true; + } + } + return false; + } + + const std::map &Spells::getCorprusSpells() const + { + return mCorprusSpells; + } + bool Spells::canUsePower(const std::string &power) const { std::map::const_iterator it = mUsedPowers.find(power); @@ -252,6 +328,30 @@ namespace MWMechanics // No need to discard spells here (doesn't really matter if non existent ids are kept) for (std::map::const_iterator it = state.mUsedPowers.begin(); it != state.mUsedPowers.end(); ++it) mUsedPowers[it->first] = MWWorld::TimeStamp(it->second); + + for (std::map >::const_iterator it = + state.mPermanentSpellEffects.begin(); it != state.mPermanentSpellEffects.end(); ++it) + { + const ESM::Spell * spell = MWBase::Environment::get().getWorld()->getStore().get().search(it->first); + if (!spell) + continue; + + mPermanentSpellEffects[it->first] = MagicEffects(); + for (std::vector::const_iterator effectIt = it->second.begin(); effectIt != it->second.end(); ++effectIt) + { + mPermanentSpellEffects[it->first].add(EffectKey(effectIt->mId, effectIt->mArg), effectIt->mMagnitude); + } + } + + mCorprusSpells.clear(); + for (std::map::const_iterator it = state.mCorprusSpells.begin(); it != state.mCorprusSpells.end(); ++it) + { + if (mSpells.find(it->first) != mSpells.end()) // Discard unavailable corprus spells + { + mCorprusSpells[it->first].mWorsenings = state.mCorprusSpells.at(it->first).mWorsenings; + mCorprusSpells[it->first].mNextWorsening = MWWorld::TimeStamp(state.mCorprusSpells.at(it->first).mNextWorsening); + } + } } void Spells::writeState(ESM::SpellState &state) const @@ -261,5 +361,26 @@ namespace MWMechanics for (std::map::const_iterator it = mUsedPowers.begin(); it != mUsedPowers.end(); ++it) state.mUsedPowers[it->first] = it->second.toEsm(); + + for (std::map::const_iterator it = mPermanentSpellEffects.begin(); it != mPermanentSpellEffects.end(); ++it) + { + std::vector effectList; + for (MagicEffects::Collection::const_iterator effectIt = it->second.begin(); effectIt != it->second.end(); ++effectIt) + { + ESM::SpellState::PermanentSpellEffectInfo info; + info.mId = effectIt->first.mId; + info.mArg = effectIt->first.mArg; + info.mMagnitude = effectIt->second.getModifier(); + + effectList.push_back(info); + } + state.mPermanentSpellEffects[it->first] = effectList; + } + + for (std::map::const_iterator it = mCorprusSpells.begin(); it != mCorprusSpells.end(); ++it) + { + state.mCorprusSpells[it->first].mWorsenings = mCorprusSpells.at(it->first).mWorsenings; + state.mCorprusSpells[it->first].mNextWorsening = mCorprusSpells.at(it->first).mNextWorsening.toEsm(); + } } } diff --git a/apps/openmw/mwmechanics/spells.hpp b/apps/openmw/mwmechanics/spells.hpp index 6997a9d7a..ab799ffe1 100644 --- a/apps/openmw/mwmechanics/spells.hpp +++ b/apps/openmw/mwmechanics/spells.hpp @@ -31,21 +31,37 @@ namespace MWMechanics { public: - typedef std::map > TContainer; // ID, typedef TContainer::const_iterator TIterator; + struct CorprusStats + { + static const int sWorseningPeriod = 24; + + int mWorsenings; + MWWorld::TimeStamp mNextWorsening; + }; + private: TContainer mSpells; + // spell-tied effects that will be applied even after removing the spell (currently used to keep positive effects when corprus is removed) + std::map mPermanentSpellEffects; + // Note: this is the spell that's about to be cast, *not* the spell selected in the GUI (which may be different) std::string mSelectedSpell; std::map mUsedPowers; + std::map mCorprusSpells; + public: + void worsenCorprus(const std::string &corpSpellId); + static bool hasCorprusEffect(const ESM::Spell *spell); + const std::map & getCorprusSpells() const; + bool canUsePower (const std::string& power) const; void usePower (const std::string& power); @@ -58,7 +74,7 @@ namespace MWMechanics TIterator end() const; - bool hasSpell(const std::string& spell) { return mSpells.find(Misc::StringUtils::lowerCase(spell)) != mSpells.end(); } + bool hasSpell(const std::string& spell) const { return mSpells.find(Misc::StringUtils::lowerCase(spell)) != mSpells.end(); } void add (const std::string& spell); ///< Adding a spell that is already listed in *this is a no-op. diff --git a/apps/openmw/mwmechanics/stat.hpp b/apps/openmw/mwmechanics/stat.hpp index 0fb4c5732..1c33db0fd 100644 --- a/apps/openmw/mwmechanics/stat.hpp +++ b/apps/openmw/mwmechanics/stat.hpp @@ -236,20 +236,20 @@ namespace MWMechanics { int mBase; int mModifier; - int mDamage; + float mDamage; // needs to be float to allow continuous damage public: AttributeValue() : mBase(0), mModifier(0), mDamage(0) {} - int getModified() const { return std::max(0, mBase - mDamage + mModifier); } + int getModified() const { return std::max(0, mBase - (int) mDamage + mModifier); } int getBase() const { return mBase; } int getModifier() const { return mModifier; } void setBase(int base) { mBase = std::max(0, base); } void setModifier(int mod) { mModifier = mod; } - void damage(int damage) { mDamage += damage; } - void restore(int amount) { mDamage -= std::min(mDamage, amount); } + void damage(float damage) { mDamage += damage; } + void restore(float amount) { mDamage -= std::min(mDamage, amount); } int getDamage() const { return mDamage; } void writeState (ESM::StatState& state) const; diff --git a/apps/openmw/mwrender/animation.cpp b/apps/openmw/mwrender/animation.cpp index a2f901b26..29db648d0 100644 --- a/apps/openmw/mwrender/animation.cpp +++ b/apps/openmw/mwrender/animation.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include @@ -70,6 +71,7 @@ Animation::Animation(const MWWorld::Ptr &ptr, Ogre::SceneNode *node) , mNonAccumCtrl(NULL) , mAccumulate(0.0f) , mNullAnimationTimePtr(OGRE_NEW NullAnimationTime) + , mGlowLight(NULL) { for(size_t i = 0;i < sNumGroups;i++) mAnimationTimePtr[i].bind(OGRE_NEW AnimationTime(this)); @@ -77,6 +79,8 @@ Animation::Animation(const MWWorld::Ptr &ptr, Ogre::SceneNode *node) Animation::~Animation() { + setLightEffect(0); + mEffects.clear(); mAnimSources.clear(); @@ -109,6 +113,11 @@ void Animation::setObjectRoot(const std::string &model, bool baseonly) mObjectRoot = (!baseonly ? NifOgre::Loader::createObjects(mInsert, mdlname) : NifOgre::Loader::createObjectBase(mInsert, mdlname)); + + // Fast forward auto-play particles, which will have been set up as Emitting by the loader. + for (unsigned int i=0; imParticles.size(); ++i) + mObjectRoot->mParticles[i]->fastForward(1, 0.1); + if(mObjectRoot->mSkelBase) { mSkelBase = mObjectRoot->mSkelBase; @@ -144,12 +153,6 @@ void Animation::setObjectRoot(const std::string &model, bool baseonly) } else mAttachedObjects.clear(); - - for(size_t i = 0;i < mObjectRoot->mControllers.size();i++) - { - if(mObjectRoot->mControllers[i].getSource().isNull()) - mObjectRoot->mControllers[i].setSource(mAnimationTimePtr[0]); - } } struct AddGlow @@ -295,7 +298,7 @@ void Animation::addAnimSource(const std::string &model) } } - if (grp == 0 && dstval->getNode()->getName() == "Bip01") + if (grp == 0 && (dstval->getNode()->getName() == "Bip01" || dstval->getNode()->getName() == "Root Bone")) { mNonAccumRoot = dstval->getNode(); mAccumRoot = mNonAccumRoot->getParent(); @@ -309,6 +312,12 @@ void Animation::addAnimSource(const std::string &model) ctrls[i].setSource(mAnimationTimePtr[grp]); grpctrls[grp].push_back(ctrls[i]); } + + for (unsigned int i = 0; i < mObjectRoot->mControllers.size(); ++i) + { + if (mObjectRoot->mControllers[i].getSource().isNull()) + mObjectRoot->mControllers[i].setSource(mAnimationTimePtr[0]); + } } void Animation::clearAnimSources() @@ -356,28 +365,40 @@ void Animation::addExtraLight(Ogre::SceneManager *sceneMgr, NifOgre::ObjectScene objlist->mControllers.push_back(Ogre::Controller(src, dest, func)); bool interior = !(mPtr.isInCell() && mPtr.getCell()->getCell()->isExterior()); - bool quadratic = fallback->getFallbackBool("LightAttenuation_OutQuadInLin") ? - !interior : fallback->getFallbackBool("LightAttenuation_UseQuadratic"); + + static bool outQuadInLin = fallback->getFallbackBool("LightAttenuation_OutQuadInLin"); + static bool useQuadratic = fallback->getFallbackBool("LightAttenuation_UseQuadratic"); + static float quadraticValue = fallback->getFallbackFloat("LightAttenuation_QuadraticValue"); + static float quadraticRadiusMult = fallback->getFallbackFloat("LightAttenuation_QuadraticRadiusMult"); + static bool useLinear = fallback->getFallbackBool("LightAttenuation_UseLinear"); + static float linearRadiusMult = fallback->getFallbackFloat("LightAttenuation_LinearRadiusMult"); + static float linearValue = fallback->getFallbackFloat("LightAttenuation_LinearValue"); + + bool quadratic = useQuadratic && (!outQuadInLin || !interior); + // with the standard 1 / (c + d*l + d*d*q) equation the attenuation factor never becomes zero, // so we ignore lights if their attenuation falls below this factor. const float threshold = 0.03; - if (!quadratic) + float quadraticAttenuation = 0; + float linearAttenuation = 0; + float activationRange = 0; + if (quadratic) { - float r = radius * fallback->getFallbackFloat("LightAttenuation_LinearRadiusMult"); - float attenuation = fallback->getFallbackFloat("LightAttenuation_LinearValue") / r; - float activationRange = 1.0f / (threshold * attenuation); - olight->setAttenuation(activationRange, 0, attenuation, 0); + float r = radius * quadraticRadiusMult; + quadraticAttenuation = quadraticValue / std::pow(r, 2); + activationRange = std::sqrt(1.0f / (threshold * quadraticAttenuation)); } - else + if (useLinear) { - float r = radius * fallback->getFallbackFloat("LightAttenuation_QuadraticRadiusMult"); - float attenuation = fallback->getFallbackFloat("LightAttenuation_QuadraticValue") / std::pow(r, 2); - float activationRange = std::sqrt(1.0f / (threshold * attenuation)); - olight->setAttenuation(activationRange, 0, 0, attenuation); + float r = radius * linearRadiusMult; + linearAttenuation = linearValue / r; + activationRange = std::max(activationRange, 1.0f / (threshold * linearAttenuation)); } + olight->setAttenuation(activationRange, 0, linearAttenuation, quadraticAttenuation); + // If there's an AttachLight bone, attach the light to that, otherwise put it in the center, if(objlist->mSkelBase && objlist->mSkelBase->getSkeleton()->hasBone("AttachLight")) objlist->mSkelBase->attachObjectToBone("AttachLight", olight); @@ -455,12 +476,29 @@ float Animation::calcAnimVelocity(const NifOgre::TextKeyMap &keys, NifOgre::Node const std::string stop = groupname+": stop"; float starttime = std::numeric_limits::max(); float stoptime = 0.0f; - NifOgre::TextKeyMap::const_iterator keyiter(keys.begin()); - while(keyiter != keys.end()) + + // Pick the last Loop Stop key and the last Loop Start key. + // This is required because of broken text keys in AshVampire.nif. + // It has *two* WalkForward: Loop Stop keys at different times, the first one is used for stopping playback + // but the animation velocity calculation uses the second one. + // As result the animation velocity calculation is not correct, and this incorrect velocity must be replicated, + // because otherwise the Creature's Speed (dagoth uthol) would not be sufficient to move fast enough. + NifOgre::TextKeyMap::const_reverse_iterator keyiter(keys.rbegin()); + while(keyiter != keys.rend()) { if(keyiter->second == start || keyiter->second == loopstart) + { starttime = keyiter->first; - else if(keyiter->second == loopstop || keyiter->second == stop) + break; + } + ++keyiter; + } + keyiter = keys.rbegin(); + while(keyiter != keys.rend()) + { + if (keyiter->second == stop) + stoptime = keyiter->first; + else if (keyiter->second == loopstop) { stoptime = keyiter->first; break; @@ -483,7 +521,7 @@ float Animation::getVelocity(const std::string &groupname) const { /* Look in reverse; last-inserted source has priority. */ AnimSourceList::const_reverse_iterator animsrc(mAnimSources.rbegin()); - for(;animsrc != mAnimSources.rend();animsrc++) + for(;animsrc != mAnimSources.rend();++animsrc) { const NifOgre::TextKeyMap &keys = (*animsrc)->mTextKeys; if(findGroupStart(keys, groupname) != keys.end()) @@ -580,31 +618,39 @@ void Animation::updatePosition(float oldtime, float newtime, Ogre::Vector3 &posi bool Animation::reset(AnimState &state, const NifOgre::TextKeyMap &keys, const std::string &groupname, const std::string &start, const std::string &stop, float startpoint) { - const NifOgre::TextKeyMap::const_iterator groupstart = findGroupStart(keys, groupname); + // Look for text keys in reverse. This normally wouldn't matter, but for some reason undeadwolf_2.nif has two + // separate walkforward keys, and the last one is supposed to be used. + NifOgre::TextKeyMap::const_reverse_iterator groupend(keys.rbegin()); + for(;groupend != keys.rend();++groupend) + { + if(groupend->second.compare(0, groupname.size(), groupname) == 0 && + groupend->second.compare(groupname.size(), 2, ": ") == 0) + break; + } std::string starttag = groupname+": "+start; - NifOgre::TextKeyMap::const_iterator startkey(groupstart); - while(startkey != keys.end() && startkey->second != starttag) + NifOgre::TextKeyMap::const_reverse_iterator startkey(groupend); + while(startkey != keys.rend() && startkey->second != starttag) ++startkey; - if(startkey == keys.end() && start == "loop start") + if(startkey == keys.rend() && start == "loop start") { starttag = groupname+": start"; - startkey = groupstart; - while(startkey != keys.end() && startkey->second != starttag) + startkey = groupend; + while(startkey != keys.rend() && startkey->second != starttag) ++startkey; } - if(startkey == keys.end()) + if(startkey == keys.rend()) return false; const std::string stoptag = groupname+": "+stop; - NifOgre::TextKeyMap::const_iterator stopkey(groupstart); - while(stopkey != keys.end() + NifOgre::TextKeyMap::const_reverse_iterator stopkey(groupend); + while(stopkey != keys.rend() // We have to ignore extra garbage at the end. // The Scrib's idle3 animation has "Idle3: Stop." instead of "Idle3: Stop". // Why, just why? :( && (stopkey->second.size() < stoptag.size() || stopkey->second.substr(0,stoptag.size()) != stoptag)) ++stopkey; - if(stopkey == keys.end()) + if(stopkey == keys.rend()) return false; if(startkey->first > stopkey->first) @@ -616,18 +662,24 @@ bool Animation::reset(AnimState &state, const NifOgre::TextKeyMap &keys, const s state.mStopTime = stopkey->first; state.mTime = state.mStartTime + ((state.mStopTime - state.mStartTime) * startpoint); + + // mLoopStartTime and mLoopStopTime normally get assigned when encountering these keys while playing the animation + // (see handleTextKey). But if startpoint is already past these keys, we need to assign them now. if(state.mTime > state.mStartTime) { const std::string loopstarttag = groupname+": loop start"; const std::string loopstoptag = groupname+": loop stop"; - NifOgre::TextKeyMap::const_iterator key(groupstart); - while(key->first <= state.mTime && key != stopkey) + + NifOgre::TextKeyMap::const_reverse_iterator key(groupend); + for (; key != startkey && key != keys.rend(); ++key) { - if(key->second == loopstarttag) + if (key->first > state.mTime) + continue; + + if (key->second == loopstarttag) state.mLoopStartTime = key->first; - else if(key->second == loopstoptag) + else if (key->second == loopstoptag) state.mLoopStopTime = key->first; - ++key; } } @@ -642,7 +694,8 @@ void split(const std::string &s, char delim, std::vector &elems) { } } -void Animation::handleTextKey(AnimState &state, const std::string &groupname, const NifOgre::TextKeyMap::const_iterator &key) +void Animation::handleTextKey(AnimState &state, const std::string &groupname, const NifOgre::TextKeyMap::const_iterator &key, + const NifOgre::TextKeyMap& textkeys) { //float time = key->first; const std::string &evt = key->second; @@ -675,7 +728,7 @@ void Animation::handleTextKey(AnimState &state, const std::string &groupname, co { MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); MWBase::SoundManager::PlayType type = MWBase::SoundManager::Play_TypeSfx; - if(evt.compare(10, evt.size()-10, "left") == 0 || evt.compare(10, evt.size()-10, "right") == 0) + if(evt.compare(10, evt.size()-10, "left") == 0 || evt.compare(10, evt.size()-10, "right") == 0 || evt.compare(10, evt.size()-10, "land") == 0) type = MWBase::SoundManager::Play_TypeFoot; sndMgr->playSound3D(mPtr, sound, volume, pitch, type); } @@ -716,6 +769,34 @@ void Animation::handleTextKey(AnimState &state, const std::string &groupname, co else mPtr.getClass().hit(mPtr); } + else if (!groupname.empty() && groupname.compare(0, groupname.size()-1, "attack") == 0 + && evt.compare(off, len, "start") == 0) + { + NifOgre::TextKeyMap::const_iterator hitKey = key; + + // Not all animations have a hit key defined. If there is none, the hit happens with the start key. + bool hasHitKey = false; + while (hitKey != textkeys.end()) + { + if (hitKey->second == groupname + ": hit") + { + hasHitKey = true; + break; + } + if (hitKey->second == groupname + ": stop") + break; + ++hitKey; + } + if (!hasHitKey) + { + if (groupname == "attack1") + mPtr.getClass().hit(mPtr, ESM::Weapon::AT_Chop); + else if (groupname == "attack2") + mPtr.getClass().hit(mPtr, ESM::Weapon::AT_Slash); + else if (groupname == "attack3") + mPtr.getClass().hit(mPtr, ESM::Weapon::AT_Thrust); + } + } else if (evt.compare(off, len, "shoot attach") == 0) attachArrow(); else if (evt.compare(off, len, "shoot release") == 0) @@ -724,7 +805,25 @@ void Animation::handleTextKey(AnimState &state, const std::string &groupname, co attachArrow(); else if (groupname == "spellcast" && evt.substr(evt.size()-7, 7) == "release") - MWBase::Environment::get().getWorld()->castSpell(mPtr); + { + // Make sure this key is actually for the RangeType we are casting. The flame atronach has + // the same animation for all range types, so there are 3 "release" keys on the same time, one for each range type. + // FIXME: This logic should really be in the CharacterController + const std::string& spellid = mPtr.getClass().getCreatureStats(mPtr).getSpells().getSelectedSpell(); + const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().find(spellid); + const ESM::ENAMstruct &effectentry = spell->mEffects.mList.at(0); + int range = 0; + if (evt.compare(off, len, "self release") == 0) + range = 0; + else if (evt.compare(off, len, "touch release") == 0) + range = 1; + else if (evt.compare(off, len, "target release") == 0) + range = 2; + if (effectentry.mRange == range) + { + MWBase::Environment::get().getWorld()->castSpell(mPtr); + } + } else if (groupname == "shield" && evt.compare(off, len, "block hit") == 0) mPtr.getClass().block(mPtr); @@ -732,8 +831,7 @@ void Animation::handleTextKey(AnimState &state, const std::string &groupname, co void Animation::changeGroups(const std::string &groupname, int groups) { - AnimStateMap::iterator stateiter = mStates.begin(); - stateiter = mStates.find(groupname); + AnimStateMap::iterator stateiter = mStates.find(groupname); if(stateiter != mStates.end()) { if(stateiter->second.mGroups != groups) @@ -792,10 +890,13 @@ void Animation::play(const std::string &groupname, int priority, int groups, boo mStates[groupname] = state; NifOgre::TextKeyMap::const_iterator textkey(textkeys.lower_bound(state.mTime)); - while(textkey != textkeys.end() && textkey->first <= state.mTime) + if (state.mPlaying) { - handleTextKey(state, groupname, textkey); - ++textkey; + while(textkey != textkeys.end() && textkey->first <= state.mTime) + { + handleTextKey(state, groupname, textkey, textkeys); + ++textkey; + } } if(state.mTime >= state.mLoopStopTime && state.mLoopCount > 0) @@ -806,10 +907,10 @@ void Animation::play(const std::string &groupname, int priority, int groups, boo if(state.mTime >= state.mLoopStopTime) break; - textkey = textkeys.lower_bound(state.mTime); + NifOgre::TextKeyMap::const_iterator textkey(textkeys.lower_bound(state.mTime)); while(textkey != textkeys.end() && textkey->first <= state.mTime) { - handleTextKey(state, groupname, textkey); + handleTextKey(state, groupname, textkey, textkeys); ++textkey; } } @@ -830,6 +931,13 @@ void Animation::play(const std::string &groupname, int priority, int groups, boo } } +void Animation::adjustSpeedMult(const std::string &groupname, float speedmult) +{ + AnimStateMap::iterator state(mStates.find(groupname)); + if(state != mStates.end()) + state->second.mSpeedMult = speedmult; +} + bool Animation::isPlaying(const std::string &groupname) const { AnimStateMap::const_iterator state(mStates.find(groupname)); @@ -905,10 +1013,10 @@ bool Animation::getInfo(const std::string &groupname, float *complete, float *sp float Animation::getStartTime(const std::string &groupname) const { - AnimSourceList::const_iterator iter(mAnimSources.begin()); - for(;iter != mAnimSources.end();iter++) + for(AnimSourceList::const_iterator iter(mAnimSources.begin()); iter != mAnimSources.end(); ++iter) { const NifOgre::TextKeyMap &keys = (*iter)->mTextKeys; + NifOgre::TextKeyMap::const_iterator found = findGroupStart(keys, groupname); if(found != keys.end()) return found->first; @@ -916,6 +1024,22 @@ float Animation::getStartTime(const std::string &groupname) const return -1.f; } +float Animation::getTextKeyTime(const std::string &textKey) const +{ + for(AnimSourceList::const_iterator iter(mAnimSources.begin()); iter != mAnimSources.end(); ++iter) + { + const NifOgre::TextKeyMap &keys = (*iter)->mTextKeys; + + for(NifOgre::TextKeyMap::const_iterator iterKey(keys.begin()); iterKey != keys.end(); ++iterKey) + { + if(iterKey->second.compare(0, textKey.size(), textKey) == 0) + return iterKey->first; + } + } + + return -1.f; +} + float Animation::getCurrentTime(const std::string &groupname) const { AnimStateMap::const_iterator iter = mStates.find(groupname); @@ -971,7 +1095,7 @@ Ogre::Vector3 Animation::runAnimation(float duration) while(textkey != textkeys.end() && textkey->first <= state.mTime) { - handleTextKey(state, stateiter->first, textkey); + handleTextKey(state, stateiter->first, textkey, textkeys); ++textkey; } @@ -985,7 +1109,7 @@ Ogre::Vector3 Animation::runAnimation(float duration) textkey = textkeys.lower_bound(state.mTime); while(textkey != textkeys.end() && textkey->first <= state.mTime) { - handleTextKey(state, stateiter->first, textkey); + handleTextKey(state, stateiter->first, textkey, textkeys); ++textkey; } @@ -999,7 +1123,11 @@ Ogre::Vector3 Animation::runAnimation(float duration) if(!state.mPlaying && state.mAutoDisable) { + if(mNonAccumCtrl && stateiter->first == mAnimationTimePtr[0]->getAnimName()) + mAccumRoot->setPosition(0.f,0.f,0.f); + mStates.erase(stateiter++); + resetActiveGroups(); } else @@ -1007,7 +1135,10 @@ Ogre::Vector3 Animation::runAnimation(float duration) } for(size_t i = 0;i < mObjectRoot->mControllers.size();i++) - mObjectRoot->mControllers[i].update(); + { + if(!mObjectRoot->mControllers[i].getSource().isNull()) + mObjectRoot->mControllers[i].update(); + } // Apply group controllers for(size_t grp = 0;grp < sNumGroups;grp++) @@ -1094,12 +1225,13 @@ void Animation::detachObjectFromBone(Ogre::MovableObject *obj) mSkelBase->detachObjectFromBone(obj); } -bool Animation::allowSwitchViewMode() const +bool Animation::upperBodyReady() const { for (AnimStateMap::const_iterator stateiter = mStates.begin(); stateiter != mStates.end(); ++stateiter) { - if(stateiter->second.mPriority > MWMechanics::Priority_Movement + if((stateiter->second.mPriority > MWMechanics::Priority_Movement && stateiter->second.mPriority < MWMechanics::Priority_Torch) + || stateiter->second.mPriority == MWMechanics::Priority_Death) return false; } return true; @@ -1112,13 +1244,7 @@ void Animation::addEffect(const std::string &model, int effectId, bool loop, con if (it->mLoop && loop && it->mEffectId == effectId && it->mBoneName == bonename) return; - // fix texture extension to .dds - if (texture.size() > 4) - { - texture[texture.size()-3] = 'd'; - texture[texture.size()-2] = 'd'; - texture[texture.size()-1] = 's'; - } + std::string correctedTexture = Misc::ResourceHelpers::correctTexturePath(texture); EffectParams params; params.mModelName = model; @@ -1141,24 +1267,72 @@ void Animation::addEffect(const std::string &model, int effectId, bool loop, con params.mObjects->mControllers[i].setSource(Ogre::SharedPtr (new EffectAnimationTime())); } - if (!texture.empty()) + + // Do some manual adjustments on the created entities/particle systems + + // It looks like vanilla MW totally ignores lighting settings for effects attached to characters. + // If we don't do this, some effects will look way too dark depending on the environment + // (e.g. magic_cast_dst.nif). They were clearly meant to use emissive lighting. + // We used to have this hack in the NIF material loader, but for effects not attached to characters + // (e.g. ash storms) the lighting settings do seem to be in use. Is there maybe a flag we have missed? + Ogre::ColourValue ambient = Ogre::ColourValue(0.f, 0.f, 0.f); + Ogre::ColourValue diffuse = Ogre::ColourValue(0.f, 0.f, 0.f); + Ogre::ColourValue specular = Ogre::ColourValue(0.f, 0.f, 0.f); + Ogre::ColourValue emissive = Ogre::ColourValue(1.f, 1.f, 1.f); + for(size_t i = 0;i < params.mObjects->mParticles.size(); ++i) { - for(size_t i = 0;i < params.mObjects->mParticles.size(); ++i) + Ogre::ParticleSystem* partSys = params.mObjects->mParticles[i]; + + Ogre::MaterialPtr mat = params.mObjects->mMaterialControllerMgr.getWritableMaterial(partSys); + + for (int t=0; tgetNumTechniques(); ++t) { - Ogre::ParticleSystem* partSys = params.mObjects->mParticles[i]; + Ogre::Technique* tech = mat->getTechnique(t); + for (int p=0; pgetNumPasses(); ++p) + { + Ogre::Pass* pass = tech->getPass(p); - Ogre::MaterialPtr mat = params.mObjects->mMaterialControllerMgr.getWritableMaterial(partSys); + pass->setAmbient(ambient); + pass->setDiffuse(diffuse); + pass->setSpecular(specular); + pass->setEmissive(emissive); - for (int t=0; tgetNumTechniques(); ++t) + if (!texture.empty()) + { + for (int tex=0; texgetNumTextureUnitStates(); ++tex) + { + Ogre::TextureUnitState* tus = pass->getTextureUnitState(tex); + tus->setTextureName(correctedTexture); + } + } + } + } + } + for(size_t i = 0;i < params.mObjects->mEntities.size(); ++i) + { + Ogre::Entity* ent = params.mObjects->mEntities[i]; + if (ent == params.mObjects->mSkelBase) + continue; + Ogre::MaterialPtr mat = params.mObjects->mMaterialControllerMgr.getWritableMaterial(ent); + + for (int t=0; tgetNumTechniques(); ++t) + { + Ogre::Technique* tech = mat->getTechnique(t); + for (int p=0; pgetNumPasses(); ++p) { - Ogre::Technique* tech = mat->getTechnique(t); - for (int p=0; pgetNumPasses(); ++p) + Ogre::Pass* pass = tech->getPass(p); + + pass->setAmbient(ambient); + pass->setDiffuse(diffuse); + pass->setSpecular(specular); + pass->setEmissive(emissive); + + if (!texture.empty()) { - Ogre::Pass* pass = tech->getPass(p); for (int tex=0; texgetNumTextureUnitStates(); ++tex) { Ogre::TextureUnitState* tus = pass->getTextureUnitState(tex); - tus->setTextureName("textures\\" + texture); + tus->setTextureName(correctedTexture); } } } @@ -1253,26 +1427,65 @@ Ogre::Vector3 Animation::getEnchantmentColor(MWWorld::Ptr item) return result; } +void Animation::setLightEffect(float effect) +{ + if (effect == 0) + { + if (mGlowLight) + { + mInsert->getCreator()->destroySceneNode(mGlowLight->getParentSceneNode()); + mInsert->getCreator()->destroyLight(mGlowLight); + mGlowLight = NULL; + } + } + else + { + if (!mGlowLight) + { + mGlowLight = mInsert->getCreator()->createLight(); + + Ogre::AxisAlignedBox bounds = Ogre::AxisAlignedBox::BOX_NULL; + for(size_t i = 0;i < mObjectRoot->mEntities.size();i++) + { + Ogre::Entity *ent = mObjectRoot->mEntities[i]; + bounds.merge(ent->getBoundingBox()); + } + mInsert->createChildSceneNode(bounds.getCenter())->attachObject(mGlowLight); + } + mGlowLight->setType(Ogre::Light::LT_POINT); + effect += 3; + mGlowLight->setAttenuation(1.0f / (0.03 * (0.5/effect)), 0, 0.5/effect, 0); + } +} + ObjectAnimation::ObjectAnimation(const MWWorld::Ptr& ptr, const std::string &model) : Animation(ptr, ptr.getRefData().getBaseNode()) { - setObjectRoot(model, false); - - Ogre::Vector3 extents = getWorldBounds().getSize(); - float size = std::max(std::max(extents.x, extents.y), extents.z); - - bool small = (size < Settings::Manager::getInt("small object size", "Viewing distance")) && - Settings::Manager::getBool("limit small object distance", "Viewing distance"); - // do not fade out doors. that will cause holes and look stupid - if(ptr.getTypeName().find("Door") != std::string::npos) - small = false; - - float dist = small ? Settings::Manager::getInt("small object distance", "Viewing distance") : 0.0f; - Ogre::Vector3 col = getEnchantmentColor(ptr); - setRenderProperties(mObjectRoot, (mPtr.getTypeName() == typeid(ESM::Static).name()) ? - (small ? RV_StaticsSmall : RV_Statics) : RV_Misc, - RQG_Main, RQG_Alpha, dist, !ptr.getClass().getEnchantment(ptr).empty(), &col); + if (!model.empty()) + { + setObjectRoot(model, false); + + Ogre::Vector3 extents = getWorldBounds().getSize(); + float size = std::max(std::max(extents.x, extents.y), extents.z); + + bool small = (size < Settings::Manager::getInt("small object size", "Viewing distance")) && + Settings::Manager::getBool("limit small object distance", "Viewing distance"); + // do not fade out doors. that will cause holes and look stupid + if(ptr.getTypeName().find("Door") != std::string::npos) + small = false; + + float dist = small ? Settings::Manager::getInt("small object distance", "Viewing distance") : 0.0f; + Ogre::Vector3 col = getEnchantmentColor(ptr); + setRenderProperties(mObjectRoot, (mPtr.getTypeName() == typeid(ESM::Static).name()) ? + (small ? RV_StaticsSmall : RV_Statics) : RV_Misc, + RQG_Main, RQG_Alpha, dist, !ptr.getClass().getEnchantment(ptr).empty(), &col); + } + else + { + // No model given. Create an object root anyway, so that lights can be added to it if needed. + mObjectRoot = NifOgre::ObjectScenePtr (new NifOgre::ObjectScene(mInsert->getCreator())); + } } void ObjectAnimation::addLight(const ESM::Light *light) @@ -1280,6 +1493,18 @@ void ObjectAnimation::addLight(const ESM::Light *light) addExtraLight(mInsert->getCreator(), mObjectRoot, light); } +void ObjectAnimation::removeParticles() +{ + for (unsigned int i=0; imParticles.size(); ++i) + { + // Don't destroyParticleSystem, the ParticleSystemController is still holding a pointer to it. + // Don't setVisible, this could conflict with a VisController. + // The following will remove all spawned particles, then set the speed factor to zero so that no new ones will be spawned. + mObjectRoot->mParticles[i]->setSpeedFactor(0.f); + mObjectRoot->mParticles[i]->clear(); + } +} + class FindEntityTransparency { public: diff --git a/apps/openmw/mwrender/animation.hpp b/apps/openmw/mwrender/animation.hpp index 564bb73ef..8ca3582dc 100644 --- a/apps/openmw/mwrender/animation.hpp +++ b/apps/openmw/mwrender/animation.hpp @@ -126,6 +126,8 @@ protected: MWWorld::Ptr mPtr; + Ogre::Light* mGlowLight; + Ogre::SceneNode *mInsert; Ogre::Entity *mSkelBase; NifOgre::ObjectScenePtr mObjectRoot; @@ -173,7 +175,8 @@ protected: const std::string &groupname, const std::string &start, const std::string &stop, float startpoint); - void handleTextKey(AnimState &state, const std::string &groupname, const NifOgre::TextKeyMap::const_iterator &key); + void handleTextKey(AnimState &state, const std::string &groupname, const NifOgre::TextKeyMap::const_iterator &key, + const NifOgre::TextKeyMap& map); /* Sets the root model of the object. If 'baseonly' is true, then any meshes or particle * systems in the model are ignored (useful for NPCs, where only the skeleton is needed for @@ -210,7 +213,7 @@ public: /** * @brief Add an effect mesh attached to a bone or the insert scene node * @param model - * @param effectId An ID for this effect. Note that adding the same ID again won't add another effect. + * @param effectId An ID for this effect by which you can identify it later. If this is not wanted, set to -1. * @param loop Loop the effect. If false, it is removed automatically after it finishes playing. If true, * you need to remove it manually using removeEffect when the effect should end. * @param bonename Bone to attach to, or empty string to use the scene node instead @@ -225,9 +228,6 @@ public: virtual void preRender (Ogre::Camera* camera); virtual void setAlpha(float alpha) {} -private: - void updateEffects(float duration); - public: void updatePtr(const MWWorld::Ptr &ptr); @@ -260,11 +260,15 @@ public: float speedmult, const std::string &start, const std::string &stop, float startpoint, size_t loops); + /** Adjust the speed multiplier of an already playing animation. + */ + void adjustSpeedMult (const std::string& groupname, float speedmult); + /** Returns true if the named animation group is playing. */ bool isPlaying(const std::string &groupname) const; - //Checks if playing any animation which shouldn't be stopped when switching camera view modes - bool allowSwitchViewMode() const; + /// Returns true if no important animations are currently playing on the upper body. + bool upperBodyReady() const; /** Gets info about the given animation group. * \param groupname Animation group to check. @@ -277,6 +281,9 @@ public: /// Get the absolute position in the animation track of the first text key with the given group. float getStartTime(const std::string &groupname) const; + /// Get the absolute position in the animation track of the text key + float getTextKeyTime(const std::string &textKey) const; + /// Get the current absolute position in the animation track for the animation that is currently playing from the given group. float getCurrentTime(const std::string& groupname) const; @@ -297,11 +304,20 @@ public: virtual Ogre::Vector3 runAnimation(float duration); + /// This is typically called as part of runAnimation, but may be called manually if needed. + void updateEffects(float duration); + + // TODO: move outside of this class + /// Makes this object glow, by placing a Light in its center. + /// @param effect Controls the radius and intensity of the light. + void setLightEffect(float effect); + virtual void showWeapons(bool showWeapon); virtual void showCarriedLeft(bool show) {} virtual void attachArrow() {} virtual void releaseArrow() {} void enableLights(bool enable); + virtual void enableHeadAnimation(bool enable) {} Ogre::AxisAlignedBox getWorldBounds(); @@ -319,6 +335,7 @@ public: ObjectAnimation(const MWWorld::Ptr& ptr, const std::string &model); void addLight(const ESM::Light *light); + void removeParticles(); bool canBatch() const; void fillBatch(Ogre::StaticGeometry *sg); diff --git a/apps/openmw/mwrender/camera.cpp b/apps/openmw/mwrender/camera.cpp index 294264951..1850df904 100644 --- a/apps/openmw/mwrender/camera.cpp +++ b/apps/openmw/mwrender/camera.cpp @@ -19,6 +19,7 @@ namespace MWRender Camera::Camera (Ogre::Camera *camera) : mCamera(camera), mCameraNode(NULL), + mCameraPosNode(NULL), mAnimation(NULL), mFirstPersonView(true), mPreviewMode(false), @@ -26,9 +27,8 @@ namespace MWRender mNearest(30.f), mFurthest(800.f), mIsNearest(false), - mIsFurthest(false), - mHeight(128.f), - mCameraDistance(300.f), + mHeight(124.f), + mCameraDistance(192.f), mDistanceAdjusted(false), mVanityToggleQueued(false), mViewModeToggleQueued(false) @@ -67,12 +67,16 @@ namespace MWRender } Ogre::Quaternion xr(Ogre::Radian(getPitch() + Ogre::Math::HALF_PI), Ogre::Vector3::UNIT_X); - if (!mVanity.enabled && !mPreviewMode) { - mCamera->getParentNode()->setOrientation(xr); - } else { + Ogre::Quaternion orient = xr; + if (mVanity.enabled || mPreviewMode) { Ogre::Quaternion zr(Ogre::Radian(getYaw()), Ogre::Vector3::UNIT_Z); - mCamera->getParentNode()->setOrientation(zr * xr); + orient = zr * xr; } + + if (isFirstPerson()) + mCamera->getParentNode()->setOrientation(orient); + else + mCameraNode->setOrientation(orient); } const std::string &Camera::getHandle() const @@ -80,20 +84,40 @@ namespace MWRender return mTrackingPtr.getRefData().getHandle(); } - void Camera::attachTo(const MWWorld::Ptr &ptr) + Ogre::SceneNode* Camera::attachTo(const MWWorld::Ptr &ptr) { mTrackingPtr = ptr; Ogre::SceneNode *node = mTrackingPtr.getRefData().getBaseNode()->createChildSceneNode(Ogre::Vector3(0.0f, 0.0f, mHeight)); + node->setInheritScale(false); + Ogre::SceneNode *posNode = node->createChildSceneNode(); + posNode->setInheritScale(false); if(mCameraNode) { node->setOrientation(mCameraNode->getOrientation()); - node->setPosition(mCameraNode->getPosition()); - node->setScale(mCameraNode->getScale()); + posNode->setPosition(mCameraPosNode->getPosition()); mCameraNode->getCreator()->destroySceneNode(mCameraNode); + mCameraNode->getCreator()->destroySceneNode(mCameraPosNode); } mCameraNode = node; - if(!mCamera->isAttached()) - mCameraNode->attachObject(mCamera); + mCameraPosNode = posNode; + + if (!isFirstPerson()) + { + mCamera->detachFromParent(); + mCameraPosNode->attachObject(mCamera); + } + + return mCameraPosNode; + } + + void Camera::setPosition(const Ogre::Vector3& position) + { + mCameraPosNode->setPosition(position); + } + + void Camera::setPosition(float x, float y, float z) + { + setPosition(Ogre::Vector3(x,y,z)); } void Camera::updateListener() @@ -107,7 +131,7 @@ namespace MWRender void Camera::update(float duration, bool paused) { - if (mAnimation->allowSwitchViewMode()) + if (mAnimation->upperBodyReady()) { // Now process the view changes we queued earlier if (mVanityToggleQueued) @@ -140,23 +164,25 @@ namespace MWRender } } - void Camera::toggleViewMode() + void Camera::toggleViewMode(bool force) { // Changing the view will stop all playing animations, so if we are playing // anything important, queue the view change for later - if (!mAnimation->allowSwitchViewMode()) + if (!mAnimation->upperBodyReady() && !force) { mViewModeToggleQueued = true; return; } + else + mViewModeToggleQueued = false; mFirstPersonView = !mFirstPersonView; processViewChange(); if (mFirstPersonView) { - mCamera->setPosition(0.f, 0.f, 0.f); + setPosition(0.f, 0.f, 0.f); } else { - mCamera->setPosition(0.f, 0.f, mCameraDistance); + setPosition(0.f, 0.f, mCameraDistance); } } @@ -171,7 +197,7 @@ namespace MWRender { // Changing the view will stop all playing animations, so if we are playing // anything important, queue the view change for later - if (!mPreviewMode) + if (isFirstPerson() && !mAnimation->upperBodyReady()) { mVanityToggleQueued = true; return false; @@ -190,14 +216,14 @@ namespace MWRender Ogre::Vector3 rot(0.f, 0.f, 0.f); if (mVanity.enabled) { rot.x = Ogre::Degree(-30.f).valueRadians(); - mMainCam.offset = mCamera->getPosition().z; + mMainCam.offset = mCameraPosNode->getPosition().z; } else { rot.x = getPitch(); offset = mMainCam.offset; } rot.z = getYaw(); - mCamera->setPosition(0.f, 0.f, offset); + setPosition(0.f, 0.f, offset); rotateCamera(rot, false); return true; @@ -205,7 +231,7 @@ namespace MWRender void Camera::togglePreviewMode(bool enable) { - if (mFirstPersonView && !mAnimation->allowSwitchViewMode()) + if (mFirstPersonView && !mAnimation->upperBodyReady()) return; if(mPreviewMode == enable) @@ -214,7 +240,7 @@ namespace MWRender mPreviewMode = enable; processViewChange(); - float offset = mCamera->getPosition().z; + float offset = mCameraPosNode->getPosition().z; if (mPreviewMode) { mMainCam.offset = offset; offset = mPreviewCam.offset; @@ -223,7 +249,7 @@ namespace MWRender offset = mMainCam.offset; } - mCamera->setPosition(0.f, 0.f, offset); + setPosition(0.f, 0.f, offset); } void Camera::setSneakOffset(float offset) @@ -282,7 +308,7 @@ namespace MWRender float Camera::getCameraDistance() const { - return mCamera->getPosition().z; + return mCameraPosNode->getPosition().z; } void Camera::setCameraDistance(float dist, bool adjust, bool override) @@ -290,23 +316,21 @@ namespace MWRender if(mFirstPersonView && !mPreviewMode && !mVanity.enabled) return; - mIsFurthest = false; mIsNearest = false; Ogre::Vector3 v(0.f, 0.f, dist); if (adjust) { - v += mCamera->getPosition(); + v += mCameraPosNode->getPosition(); } if (v.z >= mFurthest) { v.z = mFurthest; - mIsFurthest = true; } else if (!override && v.z < 10.f) { v.z = 10.f; } else if (override && v.z <= mNearest) { v.z = mNearest; mIsNearest = true; } - mCamera->setPosition(v); + setPosition(v); if (override) { if (mVanity.enabled || mPreviewMode) { @@ -323,9 +347,9 @@ namespace MWRender { if (mDistanceAdjusted) { if (mVanity.enabled || mPreviewMode) { - mCamera->setPosition(0, 0, mPreviewCam.offset); + setPosition(0, 0, mPreviewCam.offset); } else if (!mFirstPersonView) { - mCamera->setPosition(0, 0, mCameraDistance); + setPosition(0, 0, mCameraDistance); } } mDistanceAdjusted = false; @@ -356,10 +380,10 @@ namespace MWRender Ogre::TagPoint *tag = mAnimation->attachObjectToBone("Head", mCamera); tag->setInheritOrientation(false); } - else + else { mAnimation->setViewMode(NpcAnimation::VM_Normal); - mCameraNode->attachObject(mCamera); + mCameraPosNode->attachObject(mCamera); } rotateCamera(Ogre::Vector3(getPitch(), 0.f, getYaw()), false); } @@ -369,8 +393,7 @@ namespace MWRender mCamera->getParentSceneNode()->needUpdate(true); camera = mCamera->getRealPosition(); - focal = Ogre::Vector3((mCamera->getParentNode()->_getFullTransform() * - Ogre::Vector4(0.0f, 0.0f, 0.0f, 1.0f)).ptr()); + focal = mCameraNode->_getDerivedPosition(); } void Camera::togglePlayerLooking(bool enable) @@ -387,9 +410,4 @@ namespace MWRender { return mIsNearest; } - - bool Camera::isFurthest() - { - return mIsFurthest; - } } diff --git a/apps/openmw/mwrender/camera.hpp b/apps/openmw/mwrender/camera.hpp index 808f817cf..c542dc96c 100644 --- a/apps/openmw/mwrender/camera.hpp +++ b/apps/openmw/mwrender/camera.hpp @@ -27,6 +27,7 @@ namespace MWRender Ogre::Camera *mCamera; Ogre::SceneNode *mCameraNode; + Ogre::SceneNode *mCameraPosNode; NpcAnimation *mAnimation; @@ -36,7 +37,6 @@ namespace MWRender float mNearest; float mFurthest; bool mIsNearest; - bool mIsFurthest; struct { bool enabled, allowed; @@ -53,6 +53,9 @@ namespace MWRender /// Updates sound manager listener data void updateListener(); + void setPosition(const Ogre::Vector3& position); + void setPosition(float x, float y, float z); + public: Camera(Ogre::Camera *camera); ~Camera(); @@ -73,9 +76,10 @@ namespace MWRender const std::string &getHandle() const; /// Attach camera to object - void attachTo(const MWWorld::Ptr &); + Ogre::SceneNode* attachTo(const MWWorld::Ptr &); - void toggleViewMode(); + /// @param Force view mode switch, even if currently not allowed by the animation. + void toggleViewMode(bool force=false); bool toggleVanityMode(bool enable); void allowVanityMode(bool allow); @@ -117,8 +121,6 @@ namespace MWRender bool isVanityOrPreviewModeEnabled(); bool isNearest(); - - bool isFurthest(); }; } diff --git a/apps/openmw/mwrender/characterpreview.cpp b/apps/openmw/mwrender/characterpreview.cpp index 5e88b2250..d9c953133 100644 --- a/apps/openmw/mwrender/characterpreview.cpp +++ b/apps/openmw/mwrender/characterpreview.cpp @@ -37,6 +37,7 @@ namespace MWRender , mViewport(NULL) , mCamera(NULL) , mNode(NULL) + , mRecover(false) { mCharacter.mCell = NULL; } @@ -46,6 +47,16 @@ namespace MWRender } + void CharacterPreview::onFrame() + { + if (mRecover) + { + setupRenderTarget(); + mRenderTarget->update(); + mRecover = false; + } + } + void CharacterPreview::setup () { mSceneMgr = Ogre::Root::getSingleton().createSceneManager(Ogre::ST_GENERIC); @@ -74,7 +85,7 @@ namespace MWRender mNode = renderRoot->createChildSceneNode(); mAnimation = new NpcAnimation(mCharacter, mNode, - 0, true, (renderHeadOnly() ? NpcAnimation::VM_HeadOnly : NpcAnimation::VM_Normal)); + 0, true, true, (renderHeadOnly() ? NpcAnimation::VM_HeadOnly : NpcAnimation::VM_Normal)); Ogre::Vector3 scale = mNode->getScale(); mCamera->setPosition(mPosition * scale); @@ -83,19 +94,10 @@ namespace MWRender mCamera->setNearClipDistance (0.01); mCamera->setFarClipDistance (1000); - mTexture = Ogre::TextureManager::getSingleton().getByName (mName); - if (mTexture.isNull ()) - mTexture = Ogre::TextureManager::getSingleton().createManual(mName, - Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME, Ogre::TEX_TYPE_2D, mSizeX, mSizeY, 0, Ogre::PF_A8R8G8B8, Ogre::TU_RENDERTARGET); + mTexture = Ogre::TextureManager::getSingleton().createManual(mName, + Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME, Ogre::TEX_TYPE_2D, mSizeX, mSizeY, 0, Ogre::PF_A8R8G8B8, Ogre::TU_RENDERTARGET, this); - mRenderTarget = mTexture->getBuffer()->getRenderTarget(); - mRenderTarget->removeAllViewports (); - mViewport = mRenderTarget->addViewport(mCamera); - mViewport->setOverlaysEnabled(false); - mViewport->setBackgroundColour(Ogre::ColourValue(0, 0, 0, 0)); - mViewport->setShadowsEnabled(false); - mRenderTarget->setActive(true); - mRenderTarget->setAutoUpdated (false); + setupRenderTarget(); onSetup (); } @@ -107,6 +109,7 @@ namespace MWRender mSceneMgr->destroyAllCameras(); delete mAnimation; Ogre::Root::getSingleton().destroySceneManager(mSceneMgr); + Ogre::TextureManager::getSingleton().remove(mName); } } @@ -115,7 +118,7 @@ namespace MWRender assert(mAnimation); delete mAnimation; mAnimation = new NpcAnimation(mCharacter, mNode, - 0, true, (renderHeadOnly() ? NpcAnimation::VM_HeadOnly : NpcAnimation::VM_Normal)); + 0, true, true, (renderHeadOnly() ? NpcAnimation::VM_HeadOnly : NpcAnimation::VM_Normal)); float scale=1.f; mCharacter.getClass().adjustScale(mCharacter, scale); @@ -127,12 +130,39 @@ namespace MWRender onSetup(); } + void CharacterPreview::loadResource(Ogre::Resource *resource) + { + Ogre::Texture* tex = dynamic_cast(resource); + if (!tex) + return; + + tex->createInternalResources(); + + mRenderTarget = NULL; + mViewport = NULL; + mRecover = true; + } + + void CharacterPreview::setupRenderTarget() + { + mRenderTarget = mTexture->getBuffer()->getRenderTarget(); + mRenderTarget->removeAllViewports (); + mViewport = mRenderTarget->addViewport(mCamera); + mViewport->setOverlaysEnabled(false); + mViewport->setBackgroundColour(Ogre::ColourValue(0, 0, 0, 0)); + mViewport->setShadowsEnabled(false); + mRenderTarget->setActive(true); + mRenderTarget->setAutoUpdated (false); + } + // -------------------------------------------------------------------------------------------------- InventoryPreview::InventoryPreview(MWWorld::Ptr character) : CharacterPreview(character, 512, 1024, "CharacterPreview", Ogre::Vector3(0, 65, -180), Ogre::Vector3(0,65,0)) , mSelectionBuffer(NULL) + , mSizeX(0) + , mSizeY(0) { } @@ -141,7 +171,21 @@ namespace MWRender delete mSelectionBuffer; } - void InventoryPreview::update(int sizeX, int sizeY) + void InventoryPreview::resize(int sizeX, int sizeY) + { + mSizeX = sizeX; + mSizeY = sizeY; + + mViewport->setDimensions (0, 0, std::min(1.f, float(mSizeX) / float(512)), std::min(1.f, float(mSizeY) / float(1024))); + mTexture->load(); + + if (!mRenderTarget) + setupRenderTarget(); + + mRenderTarget->update(); + } + + void InventoryPreview::update() { mAnimation->updateParts(); @@ -197,15 +241,25 @@ namespace MWRender mAnimation->runAnimation(0.0f); - mViewport->setDimensions (0, 0, std::min(1.f, float(sizeX) / float(512)), std::min(1.f, float(sizeY) / float(1024))); - mNode->setOrientation (Ogre::Quaternion::IDENTITY); + mViewport->setDimensions (0, 0, std::min(1.f, float(mSizeX) / float(512)), std::min(1.f, float(mSizeY) / float(1024))); + mTexture->load(); + + if (!mRenderTarget) + setupRenderTarget(); + mRenderTarget->update(); mSelectionBuffer->update(); } + void InventoryPreview::setupRenderTarget() + { + CharacterPreview::setupRenderTarget(); + mViewport->setDimensions (0, 0, std::min(1.f, float(mSizeX) / float(512)), std::min(1.f, float(mSizeY) / float(1024))); + } + int InventoryPreview::getSlotSelected (int posX, int posY) { return mSelectionBuffer->getSelected (posX, posY); @@ -227,9 +281,9 @@ namespace MWRender RaceSelectionPreview::RaceSelectionPreview() : CharacterPreview(MWBase::Environment::get().getWorld()->getPlayerPtr(), 512, 512, "CharacterHeadPreview", Ogre::Vector3(0, 6, -35), Ogre::Vector3(0,125,0)) + , mBase (*mCharacter.get()->mBase) , mRef(&mBase) { - mBase = *mCharacter.get()->mBase; mCharacter = MWWorld::Ptr(&mRef, NULL); } @@ -243,6 +297,10 @@ namespace MWRender void RaceSelectionPreview::render() { + mTexture->load(); + + if (!mRenderTarget) + setupRenderTarget(); mRenderTarget->update(); } diff --git a/apps/openmw/mwrender/characterpreview.hpp b/apps/openmw/mwrender/characterpreview.hpp index 60312455f..711de0d15 100644 --- a/apps/openmw/mwrender/characterpreview.hpp +++ b/apps/openmw/mwrender/characterpreview.hpp @@ -22,7 +22,7 @@ namespace MWRender class NpcAnimation; - class CharacterPreview + class CharacterPreview : public Ogre::ManualResourceLoader { public: CharacterPreview(MWWorld::Ptr character, int sizeX, int sizeY, const std::string& name, @@ -34,6 +34,13 @@ namespace MWRender virtual void rebuild(); + void onFrame(); + + void loadResource(Ogre::Resource *resource); + + private: + bool mRecover; // Texture content was lost and needs to be re-rendered + private: CharacterPreview(const CharacterPreview&); CharacterPreview& operator=(const CharacterPreview&); @@ -41,6 +48,8 @@ namespace MWRender protected: virtual bool renderHeadOnly() { return false; } + virtual void setupRenderTarget(); + Ogre::TexturePtr mTexture; Ogre::RenderTarget* mRenderTarget; Ogre::Viewport* mViewport; @@ -72,11 +81,17 @@ namespace MWRender virtual ~InventoryPreview(); virtual void onSetup(); - void update(int sizeX, int sizeY); + void update(); // Render preview again, e.g. after changed equipment + void resize(int sizeX, int sizeY); int getSlotSelected(int posX, int posY); + protected: + virtual void setupRenderTarget(); + private: + int mSizeX; + int mSizeY; OEngine::Render::SelectionBuffer* mSelectionBuffer; }; diff --git a/apps/openmw/mwrender/creatureanimation.cpp b/apps/openmw/mwrender/creatureanimation.cpp index 0eb883953..fef9fa644 100644 --- a/apps/openmw/mwrender/creatureanimation.cpp +++ b/apps/openmw/mwrender/creatureanimation.cpp @@ -150,7 +150,7 @@ void CreatureWeaponAnimation::updatePart(NifOgre::ObjectScenePtr& scene, int slo } std::vector >::iterator ctrl(scene->mControllers.begin()); - for(;ctrl != scene->mControllers.end();ctrl++) + for(;ctrl != scene->mControllers.end();++ctrl) { if(ctrl->getSource().isNull()) { @@ -184,6 +184,18 @@ Ogre::Vector3 CreatureWeaponAnimation::runAnimation(float duration) { Ogre::Vector3 ret = Animation::runAnimation(duration); pitchSkeleton(mPtr.getRefData().getPosition().rot[0], mSkelBase->getSkeleton()); + + if (!mWeapon.isNull()) + { + for (unsigned int i=0; imControllers.size(); ++i) + mWeapon->mControllers[i].update(); + } + if (!mShield.isNull()) + { + for (unsigned int i=0; imControllers.size(); ++i) + mShield->mControllers[i].update(); + } + return ret; } diff --git a/apps/openmw/mwrender/effectmanager.cpp b/apps/openmw/mwrender/effectmanager.cpp index 968be0f9e..a48dea8d5 100644 --- a/apps/openmw/mwrender/effectmanager.cpp +++ b/apps/openmw/mwrender/effectmanager.cpp @@ -1,5 +1,7 @@ #include "effectmanager.hpp" +#include + #include #include #include @@ -21,15 +23,6 @@ void EffectManager::addEffect(const std::string &model, std::string textureOverr Ogre::SceneNode* sceneNode = mSceneMgr->getRootSceneNode()->createChildSceneNode(worldPosition); sceneNode->setScale(scale,scale,scale); - // fix texture extension to .dds - if (textureOverride.size() > 4) - { - textureOverride[textureOverride.size()-3] = 'd'; - textureOverride[textureOverride.size()-2] = 'd'; - textureOverride[textureOverride.size()-1] = 's'; - } - - NifOgre::ObjectScenePtr scene = NifOgre::Loader::createObjects(sceneNode, model); // TODO: turn off shadow casting @@ -44,6 +37,7 @@ void EffectManager::addEffect(const std::string &model, std::string textureOverr if (!textureOverride.empty()) { + std::string correctedTexture = Misc::ResourceHelpers::correctTexturePath(textureOverride); for(size_t i = 0;i < scene->mParticles.size(); ++i) { Ogre::ParticleSystem* partSys = scene->mParticles[i]; @@ -59,7 +53,7 @@ void EffectManager::addEffect(const std::string &model, std::string textureOverr for (int tex=0; texgetNumTextureUnitStates(); ++tex) { Ogre::TextureUnitState* tus = pass->getTextureUnitState(tex); - tus->setTextureName("textures\\" + textureOverride); + tus->setTextureName(correctedTexture); } } } diff --git a/apps/openmw/mwrender/globalmap.cpp b/apps/openmw/mwrender/globalmap.cpp index 0537ed516..fd8b91936 100644 --- a/apps/openmw/mwrender/globalmap.cpp +++ b/apps/openmw/mwrender/globalmap.cpp @@ -29,8 +29,13 @@ namespace MWRender , mWidth(0) , mHeight(0) { + mCellSize = Settings::Manager::getInt("global map cell size", "Map"); } + GlobalMap::~GlobalMap() + { + Ogre::TextureManager::getSingleton().remove(mOverlayTexture->getName()); + } void GlobalMap::render (Loading::Listener* loadingListener) { @@ -53,15 +58,20 @@ namespace MWRender mMaxY = it->getGridY(); } - int cellSize = 24; - mWidth = cellSize*(mMaxX-mMinX+1); - mHeight = cellSize*(mMaxY-mMinY+1); + mWidth = mCellSize*(mMaxX-mMinX+1); + mHeight = mCellSize*(mMaxY-mMinY+1); loadingListener->loadingOn(); loadingListener->setLabel("Creating map"); loadingListener->setProgressRange((mMaxX-mMinX+1) * (mMaxY-mMinY+1)); loadingListener->setProgress(0); + const Ogre::ColourValue waterShallowColour(0.15, 0.2, 0.19); + const Ogre::ColourValue waterDeepColour(0.1, 0.14, 0.13); + const Ogre::ColourValue groundColour(0.254, 0.19, 0.13); + const Ogre::ColourValue mountainColour(0.05, 0.05, 0.05); + const Ogre::ColourValue hillColour(0.16, 0.12, 0.08); + //if (!boost::filesystem::exists(mCacheDir + "/GlobalMap.png")) if (1) { @@ -80,35 +90,29 @@ namespace MWRender land->loadData(mask); } - for (int cellY=0; cellYmLandData->mHeights[vertexY * ESM::Land::LAND_SIZE + vertexX]; - const float mountainHeight = 15000.f; - const float hillHeight = 2500.f; if (landHeight >= 0) { + const float hillHeight = 2500.f; if (landHeight >= hillHeight) { + const float mountainHeight = 15000.f; float factor = std::min(1.f, float(landHeight-hillHeight)/mountainHeight); r = (hillColour.r * (1-factor) + mountainColour.r * factor) * 255; g = (hillColour.g * (1-factor) + mountainColour.g * factor) * 255; @@ -168,9 +172,8 @@ namespace MWRender tex->load(); - mOverlayTexture = Ogre::TextureManager::getSingleton().createManual("GlobalMapOverlay", Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME, - Ogre::TEX_TYPE_2D, mWidth, mHeight, 0, Ogre::PF_A8B8G8R8, Ogre::TU_DYNAMIC_WRITE_ONLY); + Ogre::TEX_TYPE_2D, mWidth, mHeight, 0, Ogre::PF_A8B8G8R8, Ogre::TU_DYNAMIC, this); clear(); @@ -194,9 +197,9 @@ namespace MWRender void GlobalMap::exploreCell(int cellX, int cellY) { - float originX = (cellX - mMinX) * 24; + float originX = (cellX - mMinX) * mCellSize; // NB y + 1, because we want the top left corner, not bottom left where the origin of the cell is - float originY = mHeight - (cellY+1 - mMinY) * 24; + float originY = mHeight - (cellY+1 - mMinY) * mCellSize; if (cellX > mMaxX || cellX < mMinX || cellY > mMaxY || cellY < mMinY) return; @@ -204,31 +207,47 @@ namespace MWRender Ogre::TexturePtr localMapTexture = Ogre::TextureManager::getSingleton().getByName("Cell_" + boost::lexical_cast(cellX) + "_" + boost::lexical_cast(cellY)); - // mipmap version - can't get ogre to generate automips.. - /*if (!localMapTexture.isNull()) - { - assert(localMapTexture->getBuffer(0, 4)->getWidth() == 64); // 1024 / 2^4 - - mOverlayTexture->getBuffer()->blit(localMapTexture->getBuffer(0, 4), Ogre::Image::Box(0,0,64, 64), - Ogre::Image::Box(originX,originY,originX+24,originY+24)); - }*/ - if (!localMapTexture.isNull()) { + mOverlayTexture->load(); mOverlayTexture->getBuffer()->blit(localMapTexture->getBuffer(), Ogre::Image::Box(0,0,512,512), - Ogre::Image::Box(originX,originY,originX+24,originY+24)); + Ogre::Image::Box(originX,originY,originX+mCellSize,originY+mCellSize)); + + Ogre::Image backup; + std::vector data; + data.resize(mCellSize*mCellSize*4, 0); + backup.loadDynamicImage(&data[0], mCellSize, mCellSize, Ogre::PF_A8B8G8R8); + + localMapTexture->getBuffer()->blitToMemory(Ogre::Image::Box(0,0,512,512), backup.getPixelBox()); + + for (int x=0; x buffer; - // initialize to (0,0,0,0) - buffer.resize(mWidth * mHeight, 0); + Ogre::uchar* buffer = OGRE_ALLOC_T(Ogre::uchar, mWidth * mHeight * 4, Ogre::MEMCATEGORY_GENERAL); + memset(buffer, 0, mWidth * mHeight * 4); - Ogre::PixelBox pb(mWidth, mHeight, 1, Ogre::PF_A8B8G8R8, &buffer[0]); + mOverlayImage.loadDynamicImage(&buffer[0], mWidth, mHeight, 1, Ogre::PF_A8B8G8R8, true); // pass ownership of buffer to image - mOverlayTexture->getBuffer()->blitFromMemory(pb); + mOverlayTexture->load(); + } + + void GlobalMap::loadResource(Ogre::Resource *resource) + { + Ogre::Texture* tex = dynamic_cast(resource); + Ogre::ConstImagePtrList list; + list.push_back(&mOverlayImage); + tex->_loadImages(list); } void GlobalMap::write(ESM::GlobalMap& map) @@ -238,9 +257,7 @@ namespace MWRender map.mBounds.mMinY = mMinY; map.mBounds.mMaxY = mMaxY; - Ogre::Image image; - mOverlayTexture->convertToImage(image); - Ogre::DataStreamPtr encoded = image.encode("png"); + Ogre::DataStreamPtr encoded = mOverlayImage.encode("png"); map.mImageData.resize(encoded->size()); encoded->read(&map.mImageData[0], encoded->size()); } @@ -273,7 +290,7 @@ namespace MWRender // If cell bounds of the currently loaded content and the loaded savegame do not match, // we need to resize source/dest boxes to accommodate // This means nonexisting cells will be dropped silently - int cellImageSizeDst = 24; + int cellImageSizeDst = mCellSize; // Completely off-screen? -> no need to blit anything if (bounds.mMaxX < mMinX @@ -303,8 +320,16 @@ namespace MWRender image.getHeight(), 0, Ogre::PF_A8B8G8R8); tex->loadImage(image); + mOverlayTexture->load(); mOverlayTexture->getBuffer()->blit(tex->getBuffer(), srcBox, destBox); + if (srcBox.left == destBox.left && srcBox.right == destBox.right + && srcBox.top == destBox.top && srcBox.bottom == destBox.bottom + && int(image.getWidth()) == mWidth && int(image.getHeight()) == mHeight) + mOverlayImage = image; + else + mOverlayTexture->convertToImage(mOverlayImage); + Ogre::TextureManager::getSingleton().remove("@temp"); } } diff --git a/apps/openmw/mwrender/globalmap.hpp b/apps/openmw/mwrender/globalmap.hpp index 6075d042e..b3ae85b11 100644 --- a/apps/openmw/mwrender/globalmap.hpp +++ b/apps/openmw/mwrender/globalmap.hpp @@ -18,24 +18,27 @@ namespace ESM namespace MWRender { - class GlobalMap + class GlobalMap : public Ogre::ManualResourceLoader { public: GlobalMap(const std::string& cacheDir); + ~GlobalMap(); void render(Loading::Listener* loadingListener); - int getWidth() { return mWidth; } - int getHeight() { return mHeight; } + int getWidth() const { return mWidth; } + int getHeight() const { return mHeight; } + + int getCellSize() const { return mCellSize; } void worldPosToImageSpace(float x, float z, float& imageX, float& imageY); - ///< @param x x ogre coords - /// @param z z ogre coords void cellTopLeftCornerToImageSpace(int x, int y, float& imageX, float& imageY); void exploreCell (int cellX, int cellY); + virtual void loadResource(Ogre::Resource* resource); + /// Clears the overlay void clear(); @@ -45,9 +48,12 @@ namespace MWRender private: std::string mCacheDir; + int mCellSize; + std::vector< std::pair > mExploredCells; Ogre::TexturePtr mOverlayTexture; + Ogre::Image mOverlayImage; // Backup in system memory int mWidth; int mHeight; diff --git a/apps/openmw/mwrender/localmap.cpp b/apps/openmw/mwrender/localmap.cpp index 2b0323675..f4388eec5 100644 --- a/apps/openmw/mwrender/localmap.cpp +++ b/apps/openmw/mwrender/localmap.cpp @@ -25,7 +25,7 @@ using namespace MWRender; using namespace Ogre; LocalMap::LocalMap(OEngine::Render::OgreRenderer* rend, MWRender::RenderingManager* rendering) : - mInterior(false), mCellX(0), mCellY(0) + mInterior(false) { mRendering = rend; mRenderingManager = rendering; @@ -98,6 +98,7 @@ void LocalMap::saveFogOfWar(MWWorld::CellStore* cell) return; Ogre::Image image; + tex->load(); tex->convertToImage(image); Ogre::DataStreamPtr encoded = image.encode("tga"); @@ -137,6 +138,7 @@ void LocalMap::saveFogOfWar(MWWorld::CellStore* cell) return; Ogre::Image image; + tex->load(); tex->convertToImage(image); fog->mFogTextures.push_back(ESM::FogTexture()); @@ -319,9 +321,10 @@ void LocalMap::createFogOfWar(const std::string& texturePrefix) std::vector buffer; // initialize to (0, 0, 0, 1) - buffer.resize(sFogOfWarResolution*sFogOfWarResolution, (255 << 24)); + buffer.resize(sFogOfWarResolution*sFogOfWarResolution, 0xFF000000); // upload to the texture + tex->load(); memcpy(tex->getBuffer()->lock(HardwareBuffer::HBL_DISCARD), &buffer[0], sFogOfWarResolution*sFogOfWarResolution*4); tex->getBuffer()->unlock(); @@ -340,7 +343,9 @@ Ogre::TexturePtr LocalMap::createFogOfWarTexture(const std::string &texName) sFogOfWarResolution, sFogOfWarResolution, 0, PF_A8R8G8B8, - TU_DYNAMIC_WRITE_ONLY); + TU_DYNAMIC_WRITE_ONLY, + this // ManualResourceLoader required if the texture contents are lost (due to lost devices nonsense that can occur with D3D) + ); } return tex; @@ -426,7 +431,7 @@ void LocalMap::render(const float x, const float y, mRendering->getScene()->setAmbientLight(oldAmbient); } -void LocalMap::getInteriorMapPosition (Ogre::Vector2 pos, float& nX, float& nY, int& x, int& y) +void LocalMap::worldToInteriorMapPosition (Ogre::Vector2 pos, float& nX, float& nY, int& x, int& y) { pos = rotatePoint(pos, Vector2(mBounds.getCenter().x, mBounds.getCenter().y), mAngle); @@ -439,6 +444,18 @@ void LocalMap::getInteriorMapPosition (Ogre::Vector2 pos, float& nX, float& nY, nY = 1.0-(pos.y - min.y - sSize*y)/sSize; } +Ogre::Vector2 LocalMap::interiorMapToWorldPosition (float nX, float nY, int x, int y) +{ + Vector2 min(mBounds.getMinimum().x, mBounds.getMinimum().y); + Ogre::Vector2 pos; + + pos.x = sSize * (nX + x) + min.x; + pos.y = sSize * (1.0-nY + y) + min.y; + + pos = rotatePoint(pos, Vector2(mBounds.getCenter().x, mBounds.getCenter().y), -mAngle); + return pos; +} + bool LocalMap::isPositionExplored (float nX, float nY, int x, int y, bool interior) { std::string texName = (interior ? mInteriorName + "_" : "Cell_") + coordStr(x, y); @@ -457,6 +474,30 @@ bool LocalMap::isPositionExplored (float nX, float nY, int x, int y, bool interi return alpha < 200; } +void LocalMap::loadResource(Ogre::Resource* resource) +{ + std::string resourceName = resource->getName(); + size_t pos = resourceName.find("_fog"); + if (pos != std::string::npos) + resourceName = resourceName.substr(0, pos); + if (mBuffers.find(resourceName) == mBuffers.end()) + { + // create a buffer to use for dynamic operations + std::vector buffer; + + // initialize to (0, 0, 0, 1) + buffer.resize(sFogOfWarResolution*sFogOfWarResolution, 0xFF000000); + mBuffers[resourceName] = buffer; + } + + std::vector& buffer = mBuffers[resourceName]; + + Ogre::Texture* tex = dynamic_cast(resource); + tex->createInternalResources(); + memcpy(tex->getBuffer()->lock(HardwareBuffer::HBL_DISCARD), &buffer[0], sFogOfWarResolution*sFogOfWarResolution*4); + tex->getBuffer()->unlock(); +} + void LocalMap::updatePlayer (const Ogre::Vector3& position, const Ogre::Quaternion& orientation) { if (sFogOfWarSkip != 0) @@ -473,7 +514,7 @@ void LocalMap::updatePlayer (const Ogre::Vector3& position, const Ogre::Quaterni Vector2 pos(position.x, position.y); if (mInterior) - getInteriorMapPosition(pos, u,v, x,y); + worldToInteriorMapPosition(pos, u,v, x,y); Vector3 playerdirection = mCameraRotNode->convertWorldToLocalOrientation(orientation).yAxis(); @@ -481,10 +522,9 @@ void LocalMap::updatePlayer (const Ogre::Vector3& position, const Ogre::Quaterni { x = std::ceil(pos.x / sSize)-1; y = std::ceil(pos.y / sSize)-1; - mCellX = x; - mCellY = y; } - MWBase::Environment::get().getWindowManager()->setActiveMap(x,y,mInterior); + else + MWBase::Environment::get().getWindowManager()->setActiveMap(x,y,mInterior); // convert from world coordinates to texture UV coordinates std::string texBaseName; @@ -499,7 +539,7 @@ void LocalMap::updatePlayer (const Ogre::Vector3& position, const Ogre::Quaterni texBaseName = mInteriorName + "_"; } - MWBase::Environment::get().getWindowManager()->setPlayerPos(u, v); + MWBase::Environment::get().getWindowManager()->setPlayerPos(x, y, u, v); MWBase::Environment::get().getWindowManager()->setPlayerDir(playerdirection.x, playerdirection.y); // explore radius (squared) @@ -555,6 +595,8 @@ void LocalMap::updatePlayer (const Ogre::Vector3& position, const Ogre::Quaterni } } + tex->load(); + // copy to the texture // NOTE: Could be optimized later. We actually only need to update the region that changed. // Not a big deal at the moment, the FoW is only 32x32 anyway. diff --git a/apps/openmw/mwrender/localmap.hpp b/apps/openmw/mwrender/localmap.hpp index babf7224e..4c60cbb11 100644 --- a/apps/openmw/mwrender/localmap.hpp +++ b/apps/openmw/mwrender/localmap.hpp @@ -5,6 +5,7 @@ #include #include +#include namespace MWWorld { @@ -23,12 +24,14 @@ namespace MWRender /// /// \brief Local map rendering /// - class LocalMap + class LocalMap : public Ogre::ManualResourceLoader { public: LocalMap(OEngine::Render::OgreRenderer*, MWRender::RenderingManager* rendering); ~LocalMap(); + virtual void loadResource(Ogre::Resource* resource); + /** * Clear all savegame-specific data (i.e. fog of war textures) */ @@ -58,8 +61,6 @@ namespace MWRender * Set the position & direction of the player. * @remarks This is used to draw a "fog of war" effect * to hide areas on the map the player has not discovered yet. - * @param position (OGRE coordinates) - * @param camera orientation (OGRE coordinates) */ void updatePlayer (const Ogre::Vector3& position, const Ogre::Quaternion& orientation); @@ -71,9 +72,11 @@ namespace MWRender /** * Get the interior map texture index and normalized position - * on this texture, given a world position (in ogre coordinates) + * on this texture, given a world position */ - void getInteriorMapPosition (Ogre::Vector2 pos, float& nX, float& nY, int& x, int& y); + void worldToInteriorMapPosition (Ogre::Vector2 pos, float& nX, float& nY, int& x, int& y); + + Ogre::Vector2 interiorMapToWorldPosition (float nX, float nY, int x, int y); /** * Check if a given position is explored by the player (i.e. not obscured by fog of war) @@ -131,7 +134,6 @@ namespace MWRender Ogre::RenderTarget* mRenderTarget; bool mInterior; - int mCellX, mCellY; Ogre::AxisAlignedBox mBounds; std::string mInteriorName; }; diff --git a/apps/openmw/mwrender/npcanimation.cpp b/apps/openmw/mwrender/npcanimation.cpp index 03ccde388..c43d3663e 100644 --- a/apps/openmw/mwrender/npcanimation.cpp +++ b/apps/openmw/mwrender/npcanimation.cpp @@ -66,15 +66,74 @@ std::string getVampireHead(const std::string& race, bool female) namespace MWRender { -float HeadAnimationTime::getValue() const +HeadAnimationTime::HeadAnimationTime(MWWorld::Ptr reference) + : mReference(reference), mTalkStart(0), mTalkStop(0), mBlinkStart(0), mBlinkStop(0), mValue(0), mEnabled(true) +{ + resetBlinkTimer(); +} + +void HeadAnimationTime::setEnabled(bool enabled) { - // TODO use time from text keys (Talk Start/Stop, Blink Start/Stop) - // TODO: Handle eye blinking + mEnabled = enabled; +} + +void HeadAnimationTime::resetBlinkTimer() +{ + mBlinkTimer = -(2 + (std::rand() / double(RAND_MAX*1.0)) * 6); +} + +void HeadAnimationTime::update(float dt) +{ + if (!mEnabled) + return; + if (MWBase::Environment::get().getSoundManager()->sayDone(mReference)) - return 0; + { + mBlinkTimer += dt; + + float duration = mBlinkStop - mBlinkStart; + + if (mBlinkTimer >= 0 && mBlinkTimer <= duration) + { + mValue = mBlinkStart + mBlinkTimer; + } + else + mValue = mBlinkStop; + + if (mBlinkTimer > duration) + resetBlinkTimer(); + } else - // TODO: Use the loudness of the currently playing sound - return 1; + { + mValue = mTalkStart + + (mTalkStop - mTalkStart) * + std::min(1.f, MWBase::Environment::get().getSoundManager()->getSaySoundLoudness(mReference)*2); // Rescale a bit (most voices are not very loud) + } +} + +float HeadAnimationTime::getValue() const +{ + return mValue; +} + +void HeadAnimationTime::setTalkStart(float value) +{ + mTalkStart = value; +} + +void HeadAnimationTime::setTalkStop(float value) +{ + mTalkStop = value; +} + +void HeadAnimationTime::setBlinkStart(float value) +{ + mBlinkStart = value; +} + +void HeadAnimationTime::setBlinkStop(float value) +{ + mBlinkStop = value; } static NpcAnimation::PartBoneMap createPartListMap() @@ -118,7 +177,7 @@ NpcAnimation::~NpcAnimation() } -NpcAnimation::NpcAnimation(const MWWorld::Ptr& ptr, Ogre::SceneNode* node, int visibilityFlags, bool disableListener, ViewMode viewMode) +NpcAnimation::NpcAnimation(const MWWorld::Ptr& ptr, Ogre::SceneNode* node, int visibilityFlags, bool disableListener, bool disableSounds, ViewMode viewMode) : Animation(ptr, node), mVisibilityFlags(visibilityFlags), mListenerDisabled(disableListener), @@ -127,7 +186,8 @@ NpcAnimation::NpcAnimation(const MWWorld::Ptr& ptr, Ogre::SceneNode* node, int v mShowCarriedLeft(true), mFirstPersonOffset(0.f, 0.f, 0.f), mAlpha(1.f), - mNpcType(Type_Normal) + mNpcType(Type_Normal), + mSoundsDisabled(disableSounds) { mNpc = mPtr.get()->mBase; @@ -140,10 +200,10 @@ NpcAnimation::NpcAnimation(const MWWorld::Ptr& ptr, Ogre::SceneNode* node, int v mPartPriorities[i] = 0; } + updateNpcBase(); + if (!disableListener) mPtr.getClass().getInventoryStore(mPtr).setListener(this, mPtr); - - updateNpcBase(); } void NpcAnimation::setViewMode(NpcAnimation::ViewMode viewMode) @@ -238,10 +298,9 @@ void NpcAnimation::updateParts() { mAlpha = 1.f; const MWWorld::Class &cls = mPtr.getClass(); - MWWorld::InventoryStore &inv = cls.getInventoryStore(mPtr); NpcType curType = Type_Normal; - if (cls.getCreatureStats(mPtr).getMagicEffects().get(ESM::MagicEffect::Vampirism).mMagnitude > 0) + if (cls.getCreatureStats(mPtr).getMagicEffects().get(ESM::MagicEffect::Vampirism).getMagnitude() > 0) curType = Type_Vampire; if (cls.getNpcStats(mPtr).isWerewolf()) curType = Type_Werewolf; @@ -275,6 +334,7 @@ void NpcAnimation::updateParts() }; static const size_t slotlistsize = sizeof(slotlist)/sizeof(slotlist[0]); + MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); for(size_t i = 0;i < slotlistsize && mViewMode != VM_HeadOnly;i++) { MWWorld::ContainerStoreIterator store = inv.getSlot(slotlist[i].mSlot); @@ -308,7 +368,7 @@ void NpcAnimation::updateParts() ESM::PartReferenceType parts[] = { ESM::PRT_Groin, ESM::PRT_Skirt, ESM::PRT_RLeg, ESM::PRT_LLeg, ESM::PRT_RUpperarm, ESM::PRT_LUpperarm, ESM::PRT_RKnee, ESM::PRT_LKnee, - ESM::PRT_RForearm, ESM::PRT_LForearm, ESM::PRT_RPauldron, ESM::PRT_LPauldron + ESM::PRT_RForearm, ESM::PRT_LForearm }; size_t parts_size = sizeof(parts)/sizeof(parts[0]); for(size_t p = 0;p < parts_size;++p) @@ -351,17 +411,18 @@ void NpcAnimation::updateParts() // Remember body parts so we only have to search through the store once for each race/gender/viewmode combination static std::map< std::pair,std::vector > sRaceMapping; - static std::map , std::vector > sVampireMapping; - - static const int Flag_Female = 1<<0; - static const int Flag_FirstPerson = 1<<1; - bool isWerewolf = (mNpcType == Type_Werewolf); int flags = (isWerewolf ? -1 : 0); if(!mNpc->isMale()) + { + static const int Flag_Female = 1<<0; flags |= Flag_Female; + } if(mViewMode == VM_FirstPerson) + { + static const int Flag_FirstPerson = 1<<1; flags |= Flag_FirstPerson; + } std::string race = (isWerewolf ? "werewolf" : Misc::StringUtils::lowerCase(mNpc->mRace)); std::pair thisCombination = std::make_pair(race, flags); @@ -408,8 +469,6 @@ void NpcAnimation::updateParts() if (bodypart.mData.mType != ESM::BodyPart::MT_Skin) continue; - if (!mNpc->isMale() != (bodypart.mData.mFlags & ESM::BodyPart::BPF_Female)) - continue; if (!Misc::StringUtils::ciEqual(bodypart.mRace, mNpc->mRace)) continue; @@ -435,6 +494,20 @@ void NpcAnimation::updateParts() } continue; } + + if (!mNpc->isMale() != (bodypart.mData.mFlags & ESM::BodyPart::BPF_Female)) + { + // Allow opposite gender's parts as fallback if parts for our gender are missing + BodyPartMapType::const_iterator bIt = sBodyPartMap.lower_bound(BodyPartMapType::key_type(bodypart.mData.mPart)); + while(bIt != sBodyPartMap.end() && bIt->first == bodypart.mData.mPart) + { + if(!parts[bIt->second]) + parts[bIt->second] = &*it; + ++bIt; + } + continue; + } + BodyPartMapType::const_iterator bIt = sBodyPartMap.lower_bound(BodyPartMapType::key_type(bodypart.mData.mPart)); while(bIt != sBodyPartMap.end() && bIt->first == bodypart.mData.mPart) { @@ -483,6 +556,10 @@ NifOgre::ObjectScenePtr NpcAnimation::insertBoundedPart(const std::string &model std::for_each(objects->mEntities.begin(), objects->mEntities.end(), SetObjectGroup(group)); std::for_each(objects->mParticles.begin(), objects->mParticles.end(), SetObjectGroup(group)); + // Fast forward auto-play particles, which will have been set up as Emitting by the loader. + for (unsigned int i=0; imParticles.size(); ++i) + objects->mParticles[i]->fastForward(1, 0.1); + if(objects->mSkelBase) { Ogre::AnimationStateSet *aset = objects->mSkelBase->getAllAnimationStates(); @@ -506,6 +583,8 @@ Ogre::Vector3 NpcAnimation::runAnimation(float timepassed) { Ogre::Vector3 ret = Animation::runAnimation(timepassed); + mHeadAnimationTime->update(timepassed); + Ogre::SkeletonInstance *baseinst = mSkelBase->getSkeleton(); if(mViewMode == VM_FirstPerson) { @@ -529,7 +608,7 @@ Ogre::Vector3 NpcAnimation::runAnimation(float timepassed) if (mObjectParts[i].isNull()) continue; std::vector >::iterator ctrl(mObjectParts[i]->mControllers.begin()); - for(;ctrl != mObjectParts[i]->mControllers.end();ctrl++) + for(;ctrl != mObjectParts[i]->mControllers.end();++ctrl) ctrl->update(); Ogre::Entity *ent = mObjectParts[i]->mSkelBase; @@ -547,6 +626,11 @@ void NpcAnimation::removeIndividualPart(ESM::PartReferenceType type) mPartslots[type] = -1; mObjectParts[type].setNull(); + if (!mSoundIds[type].empty() && !mSoundsDisabled) + { + MWBase::Environment::get().getSoundManager()->stopSound3D(mPtr, mSoundIds[type]); + mSoundIds[type].clear(); + } } void NpcAnimation::reserveIndividualPart(ESM::PartReferenceType type, int group, int priority) @@ -576,8 +660,22 @@ bool NpcAnimation::addOrReplaceIndividualPart(ESM::PartReferenceType type, int g removeIndividualPart(type); mPartslots[type] = group; mPartPriorities[type] = priority; - mObjectParts[type] = insertBoundedPart(mesh, group, sPartList.at(type), enchantedGlow, glowColor); + + if (!mSoundsDisabled) + { + MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); + MWWorld::ContainerStoreIterator csi = inv.getSlot(group < 0 ? MWWorld::InventoryStore::Slot_Helmet : group); + if (csi != inv.end()) + { + mSoundIds[type] = csi->getClass().getSound(*csi); + if (!mSoundIds[type].empty()) + { + MWBase::Environment::get().getSoundManager()->playSound3D(mPtr, mSoundIds[type], 1.0f, 1.0f, MWBase::SoundManager::Play_TypeSfx, + MWBase::SoundManager::Play_Loop); + } + } + } if(mObjectParts[type]->mSkelBase) { Ogre::SkeletonInstance *skel = mObjectParts[type]->mSkelBase->getSkeleton(); @@ -603,14 +701,28 @@ bool NpcAnimation::addOrReplaceIndividualPart(ESM::PartReferenceType type, int g } std::vector >::iterator ctrl(mObjectParts[type]->mControllers.begin()); - for(;ctrl != mObjectParts[type]->mControllers.end();ctrl++) + for(;ctrl != mObjectParts[type]->mControllers.end();++ctrl) { if(ctrl->getSource().isNull()) { ctrl->setSource(mNullAnimationTimePtr); if (type == ESM::PRT_Head) + { ctrl->setSource(mHeadAnimationTime); + const NifOgre::TextKeyMap& keys = mObjectParts[type]->mTextKeys; + for (NifOgre::TextKeyMap::const_iterator it = keys.begin(); it != keys.end(); ++it) + { + if (Misc::StringUtils::ciEqual(it->second, "talk: start")) + mHeadAnimationTime->setTalkStart(it->first); + if (Misc::StringUtils::ciEqual(it->second, "talk: stop")) + mHeadAnimationTime->setTalkStop(it->first); + if (Misc::StringUtils::ciEqual(it->second, "blink: start")) + mHeadAnimationTime->setBlinkStart(it->first); + if (Misc::StringUtils::ciEqual(it->second, "blink: stop")) + mHeadAnimationTime->setBlinkStop(it->first); + } + } else if (type == ESM::PRT_Weapon) ctrl->setSource(mWeaponAnimationTime); } @@ -626,7 +738,7 @@ void NpcAnimation::addPartGroup(int group, int priority, const std::vector::const_iterator part(parts.begin()); - for(;part != parts.end();part++) + for(;part != parts.end();++part) { const ESM::BodyPart *bodypart = 0; if(!mNpc->isMale() && !part->mFemale.empty()) @@ -668,7 +780,7 @@ void NpcAnimation::showWeapons(bool showWeapon) mShowWeapons = showWeapon; if(showWeapon) { - MWWorld::InventoryStore &inv = mPtr.getClass().getInventoryStore(mPtr); + MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); MWWorld::ContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); if(weapon != inv.end()) { @@ -701,9 +813,8 @@ void NpcAnimation::showWeapons(bool showWeapon) void NpcAnimation::showCarriedLeft(bool show) { mShowCarriedLeft = show; - MWWorld::InventoryStore &inv = mPtr.getClass().getInventoryStore(mPtr); + MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); MWWorld::ContainerStoreIterator iter = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); - if(show && iter != inv.end()) { Ogre::Vector3 glowColor = getEnchantmentColor(*iter); @@ -787,6 +898,11 @@ void NpcAnimation::setAlpha(float alpha) } } +void NpcAnimation::enableHeadAnimation(bool enable) +{ + mHeadAnimationTime->setEnabled(enable); +} + void NpcAnimation::preRender(Ogre::Camera *camera) { Animation::preRender(camera); @@ -831,4 +947,9 @@ void NpcAnimation::applyAlpha(float alpha, Ogre::Entity *ent, NifOgre::ObjectSce } } +void NpcAnimation::equipmentChanged() +{ + updateParts(); +} + } diff --git a/apps/openmw/mwrender/npcanimation.hpp b/apps/openmw/mwrender/npcanimation.hpp index 8ec46facd..ee62fce9c 100644 --- a/apps/openmw/mwrender/npcanimation.hpp +++ b/apps/openmw/mwrender/npcanimation.hpp @@ -19,8 +19,29 @@ class HeadAnimationTime : public Ogre::ControllerValue { private: MWWorld::Ptr mReference; + float mTalkStart; + float mTalkStop; + float mBlinkStart; + float mBlinkStop; + + float mBlinkTimer; + + bool mEnabled; + + float mValue; +private: + void resetBlinkTimer(); public: - HeadAnimationTime(MWWorld::Ptr reference) : mReference(reference) {} + HeadAnimationTime(MWWorld::Ptr reference); + + void update(float dt); + + void setEnabled(bool enabled); + + void setTalkStart(float value); + void setTalkStop(float value); + void setBlinkStart(float value); + void setBlinkStop(float value); virtual Ogre::Real getValue() const; virtual void setValue(Ogre::Real value) @@ -30,7 +51,7 @@ public: class NpcAnimation : public Animation, public WeaponAnimation, public MWWorld::InventoryStoreListener { public: - virtual void equipmentChanged() { updateParts(); } + virtual void equipmentChanged(); virtual void permanentEffectAdded(const ESM::MagicEffect *magicEffect, bool isNew, bool playSound); public: @@ -49,6 +70,7 @@ private: // Bounded Parts NifOgre::ObjectScenePtr mObjectParts[ESM::PRT_Count]; + std::string mSoundIds[ESM::PRT_Count]; const ESM::NPC *mNpc; std::string mHeadModel; @@ -76,6 +98,7 @@ private: Ogre::SharedPtr mWeaponAnimationTime; float mAlpha; + bool mSoundsDisabled; void updateNpcBase(); @@ -102,12 +125,15 @@ public: * one listener at a time, so you shouldn't do this if creating several NpcAnimations * for the same Ptr, eg preview dolls for the player. * Those need to be manually rendered anyway. + * @param disableSounds Same as \a disableListener but for playing items sounds * @param viewMode */ NpcAnimation(const MWWorld::Ptr& ptr, Ogre::SceneNode* node, int visibilityFlags, bool disableListener = false, - ViewMode viewMode=VM_Normal); + bool disableSounds = false, ViewMode viewMode=VM_Normal); virtual ~NpcAnimation(); + virtual void enableHeadAnimation(bool enable); + virtual void setWeaponGroup(const std::string& group) { mWeaponAnimationTime->setGroup(group); } virtual Ogre::Vector3 runAnimation(float timepassed); diff --git a/apps/openmw/mwrender/objects.cpp b/apps/openmw/mwrender/objects.cpp index 3101100f2..96decbb36 100644 --- a/apps/openmw/mwrender/objects.cpp +++ b/apps/openmw/mwrender/objects.cpp @@ -73,85 +73,93 @@ void Objects::insertBegin(const MWWorld::Ptr& ptr) ptr.getRefData().setBaseNode(insert); } -void Objects::insertModel(const MWWorld::Ptr &ptr, const std::string &mesh) +void Objects::insertModel(const MWWorld::Ptr &ptr, const std::string &mesh, bool batch, bool addLight) { insertBegin(ptr); std::auto_ptr anim(new ObjectAnimation(ptr, mesh)); - Ogre::AxisAlignedBox bounds = anim->getWorldBounds(); - Ogre::Vector3 extents = bounds.getSize(); - extents *= ptr.getRefData().getBaseNode()->getScale(); - float size = std::max(std::max(extents.x, extents.y), extents.z); - - bool small = (size < Settings::Manager::getInt("small object size", "Viewing distance")) && - Settings::Manager::getBool("limit small object distance", "Viewing distance"); - // do not fade out doors. that will cause holes and look stupid - if(ptr.getTypeName().find("Door") != std::string::npos) - small = false; - - if (mBounds.find(ptr.getCell()) == mBounds.end()) - mBounds[ptr.getCell()] = Ogre::AxisAlignedBox::BOX_NULL; - mBounds[ptr.getCell()].merge(bounds); - if(ptr.getTypeName() == typeid(ESM::Light).name()) - anim->addLight(ptr.get()->mBase); - - if(ptr.getTypeName() == typeid(ESM::Static).name() && - Settings::Manager::getBool("use static geometry", "Objects") && - anim->canBatch()) { - Ogre::StaticGeometry* sg = 0; + if (addLight) + anim->addLight(ptr.get()->mBase); + else + anim->removeParticles(); + } - if (small) + if (!mesh.empty()) + { + Ogre::AxisAlignedBox bounds = anim->getWorldBounds(); + Ogre::Vector3 extents = bounds.getSize(); + extents *= ptr.getRefData().getBaseNode()->getScale(); + float size = std::max(std::max(extents.x, extents.y), extents.z); + + bool small = (size < Settings::Manager::getInt("small object size", "Viewing distance")) && + Settings::Manager::getBool("limit small object distance", "Viewing distance"); + // do not fade out doors. that will cause holes and look stupid + if(ptr.getTypeName().find("Door") != std::string::npos) + small = false; + + if (mBounds.find(ptr.getCell()) == mBounds.end()) + mBounds[ptr.getCell()] = Ogre::AxisAlignedBox::BOX_NULL; + mBounds[ptr.getCell()].merge(bounds); + + if(batch && + Settings::Manager::getBool("use static geometry", "Objects") && + anim->canBatch()) { - if(mStaticGeometrySmall.find(ptr.getCell()) == mStaticGeometrySmall.end()) - { - uniqueID = uniqueID+1; - sg = mRenderer.getScene()->createStaticGeometry("sg" + Ogre::StringConverter::toString(uniqueID)); - sg->setOrigin(ptr.getRefData().getBaseNode()->getPosition()); - mStaticGeometrySmall[ptr.getCell()] = sg; + Ogre::StaticGeometry* sg = 0; - sg->setRenderingDistance(Settings::Manager::getInt("small object distance", "Viewing distance")); + if (small) + { + if(mStaticGeometrySmall.find(ptr.getCell()) == mStaticGeometrySmall.end()) + { + uniqueID = uniqueID+1; + sg = mRenderer.getScene()->createStaticGeometry("sg" + Ogre::StringConverter::toString(uniqueID)); + sg->setOrigin(ptr.getRefData().getBaseNode()->getPosition()); + mStaticGeometrySmall[ptr.getCell()] = sg; + + sg->setRenderingDistance(Settings::Manager::getInt("small object distance", "Viewing distance")); + } + else + sg = mStaticGeometrySmall[ptr.getCell()]; } else - sg = mStaticGeometrySmall[ptr.getCell()]; - } - else - { - if(mStaticGeometry.find(ptr.getCell()) == mStaticGeometry.end()) { - uniqueID = uniqueID+1; - sg = mRenderer.getScene()->createStaticGeometry("sg" + Ogre::StringConverter::toString(uniqueID)); - sg->setOrigin(ptr.getRefData().getBaseNode()->getPosition()); - mStaticGeometry[ptr.getCell()] = sg; + if(mStaticGeometry.find(ptr.getCell()) == mStaticGeometry.end()) + { + uniqueID = uniqueID+1; + sg = mRenderer.getScene()->createStaticGeometry("sg" + Ogre::StringConverter::toString(uniqueID)); + sg->setOrigin(ptr.getRefData().getBaseNode()->getPosition()); + mStaticGeometry[ptr.getCell()] = sg; + } + else + sg = mStaticGeometry[ptr.getCell()]; } - else - sg = mStaticGeometry[ptr.getCell()]; - } - // This specifies the size of a single batch region. - // If it is set too high: - // - there will be problems choosing the correct lights - // - the culling will be more inefficient - // If it is set too low: - // - there will be too many batches. - if(ptr.getCell()->isExterior()) - sg->setRegionDimensions(Ogre::Vector3(2048,2048,2048)); - else - sg->setRegionDimensions(Ogre::Vector3(1024,1024,1024)); + // This specifies the size of a single batch region. + // If it is set too high: + // - there will be problems choosing the correct lights + // - the culling will be more inefficient + // If it is set too low: + // - there will be too many batches. + if(ptr.getCell()->isExterior()) + sg->setRegionDimensions(Ogre::Vector3(2048,2048,2048)); + else + sg->setRegionDimensions(Ogre::Vector3(1024,1024,1024)); - sg->setVisibilityFlags(small ? RV_StaticsSmall : RV_Statics); + sg->setVisibilityFlags(small ? RV_StaticsSmall : RV_Statics); - sg->setCastShadows(true); + sg->setCastShadows(true); - sg->setRenderQueueGroup(RQG_Main); + sg->setRenderQueueGroup(RQG_Main); - anim->fillBatch(sg); - /* TODO: We could hold on to this and just detach it from the scene graph, so if the Ptr - * ever needs to modify we can reattach it and rebuild the StaticGeometry object without - * it. Would require associating the Ptr with the StaticGeometry. */ - anim.reset(); + anim->fillBatch(sg); + /* TODO: We could hold on to this and just detach it from the scene graph, so if the Ptr + * ever needs to modify we can reattach it and rebuild the StaticGeometry object without + * it. Would require associating the Ptr with the StaticGeometry. */ + anim.reset(); + } } if(anim.get() != NULL) diff --git a/apps/openmw/mwrender/objects.hpp b/apps/openmw/mwrender/objects.hpp index 665a1e657..7f740dbab 100644 --- a/apps/openmw/mwrender/objects.hpp +++ b/apps/openmw/mwrender/objects.hpp @@ -41,7 +41,7 @@ public: , mRootNode(NULL) {} ~Objects(){} - void insertModel(const MWWorld::Ptr& ptr, const std::string &model); + void insertModel(const MWWorld::Ptr& ptr, const std::string &model, bool batch=false, bool addLight=false); ObjectAnimation* getAnimation(const MWWorld::Ptr &ptr); diff --git a/apps/openmw/mwrender/occlusionquery.cpp b/apps/openmw/mwrender/occlusionquery.cpp index 92a49acc0..2693d68b2 100644 --- a/apps/openmw/mwrender/occlusionquery.cpp +++ b/apps/openmw/mwrender/occlusionquery.cpp @@ -19,14 +19,14 @@ using namespace Ogre; OcclusionQuery::OcclusionQuery(OEngine::Render::OgreRenderer* renderer, SceneNode* sunNode) : mSunTotalAreaQuery(0), mSunVisibleAreaQuery(0), mActiveQuery(0), - mDoQuery(0), mSunVisibility(0), + mBBQueryVisible(0), mBBQueryTotal(0), mSunNode(sunNode), mBBNodeReal(0), + mSunVisibility(0), mWasVisible(false), mActive(false), - mFirstFrame(true) + mFirstFrame(true), + mDoQuery(0), + mRendering(renderer) { - mRendering = renderer; - mSunNode = sunNode; - try { RenderSystem* renderSystem = Root::getSingleton().getRenderSystem(); diff --git a/apps/openmw/mwrender/refraction.cpp b/apps/openmw/mwrender/refraction.cpp index f22968e9d..164380866 100644 --- a/apps/openmw/mwrender/refraction.cpp +++ b/apps/openmw/mwrender/refraction.cpp @@ -33,9 +33,9 @@ namespace MWRender Ogre::Viewport* vp = mRenderTarget->addViewport(mCamera); vp->setOverlaysEnabled(false); vp->setShadowsEnabled(false); - vp->setVisibilityMask(RV_Actors + RV_Misc + RV_Statics + RV_StaticsSmall + RV_Terrain + RV_Sky); + vp->setVisibilityMask(RV_Actors + RV_Misc + RV_Statics + RV_StaticsSmall + RV_Terrain + RV_Sky + RV_FirstPerson); vp->setMaterialScheme("water_refraction"); - vp->setBackgroundColour (Ogre::ColourValue(0.18039, 0.23137, 0.25490)); + vp->setBackgroundColour (Ogre::ColourValue(0.090195, 0.115685, 0.12745)); mRenderTarget->setAutoUpdated(true); mRenderTarget->addListener(this); } @@ -50,7 +50,8 @@ namespace MWRender void Refraction::preRenderTargetUpdate(const Ogre::RenderTargetEvent& evt) { - mParentCamera->getParentSceneNode ()->needUpdate (); + if (mParentCamera->isAttached()) + mParentCamera->getParentSceneNode ()->needUpdate (); mCamera->setOrientation(mParentCamera->getDerivedOrientation()); mCamera->setPosition(mParentCamera->getDerivedPosition()); mCamera->setNearClipDistance(mParentCamera->getNearClipDistance()); diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index 882a3d09b..ed25e70a6 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -22,7 +22,8 @@ #include #include -#include +#include +#include #include "../mwworld/esmstore.hpp" #include "../mwworld/class.hpp" @@ -35,6 +36,7 @@ #include "../mwbase/statemanager.hpp" #include "../mwmechanics/creaturestats.hpp" +#include "../mwmechanics/npcstats.hpp" #include "../mwworld/ptr.hpp" @@ -62,6 +64,7 @@ RenderingManager::RenderingManager(OEngine::Render::OgreRenderer& _rend, const b , mPhysicsEngine(engine) , mTerrain(NULL) , mEffectManager(NULL) + , mRenderWorld(true) { mActors = new MWRender::Actors(mRendering, this); mObjects = new MWRender::Objects(mRendering); @@ -112,22 +115,19 @@ RenderingManager::RenderingManager(OEngine::Render::OgreRenderer& _rend, const b mFactory->loadAllFiles(); - // Compressed textures with 0 mip maps are bugged in 1.8, so disable mipmap generator in that case - // ( https://ogre3d.atlassian.net/browse/OGRE-259 ) -#if OGRE_VERSION >= (1 << 16 | 9 << 8 | 0) TextureManager::getSingleton().setDefaultNumMipmaps(Settings::Manager::getInt("num mipmaps", "General")); -#else - TextureManager::getSingleton().setDefaultNumMipmaps(0); -#endif // Set default texture filtering options TextureFilterOptions tfo; std::string filter = Settings::Manager::getString("texture filtering", "General"); +#ifndef ANDROID if (filter == "anisotropic") tfo = TFO_ANISOTROPIC; else if (filter == "trilinear") tfo = TFO_TRILINEAR; else if (filter == "bilinear") tfo = TFO_BILINEAR; else /*if (filter == "none")*/ tfo = TFO_NONE; - +#else + tfo = TFO_NONE; +#endif MaterialManager::getSingleton().setDefaultTextureFiltering(tfo); MaterialManager::getSingleton().setDefaultAnisotropy( (filter == "anisotropic") ? Settings::Manager::getInt("anisotropy", "General") : 1 ); @@ -211,11 +211,6 @@ MWRender::Actors& RenderingManager::getActors(){ return *mActors; } -OEngine::Render::Fader* RenderingManager::getFader() -{ - return mRendering.getFader(); -} - MWRender::Camera* RenderingManager::getCamera() const { return mCamera; @@ -223,6 +218,9 @@ MWRender::Camera* RenderingManager::getCamera() const void RenderingManager::removeCell (MWWorld::CellStore *store) { + if (store->isExterior()) + mTerrain->unloadCell(store->getCell()->getGridX(), store->getCell()->getGridY()); + mLocalMap->saveFogOfWar(store); mObjects->removeCell(store); mActors->removeCell(store); @@ -239,12 +237,23 @@ bool RenderingManager::toggleWater() return mWater->toggle(); } +bool RenderingManager::toggleWorld() +{ + mRenderWorld = !mRenderWorld; + + int visibilityMask = mRenderWorld ? ~int(0) : 0; + mRendering.getViewport()->setVisibilityMask(visibilityMask); + return mRenderWorld; +} + void RenderingManager::cellAdded (MWWorld::CellStore *store) { + if (store->isExterior()) + mTerrain->loadCell(store->getCell()->getGridX(), store->getCell()->getGridY()); + mObjects->buildStaticGeometry (*store); sh::Factory::getInstance().unloadUnreferencedMaterials(); mDebugging->cellAdded(store); - waterAdded(store); } void RenderingManager::addObject (const MWWorld::Ptr& ptr){ @@ -306,7 +315,7 @@ void RenderingManager::updatePlayerPtr(const MWWorld::Ptr &ptr) if(mPlayerAnimation) mPlayerAnimation->updatePtr(ptr); if(mCamera->getHandle() == ptr.getRefData().getHandle()) - mCamera->attachTo(ptr); + attachCameraTo(ptr); } void RenderingManager::rebuildPtr(const MWWorld::Ptr &ptr) @@ -321,7 +330,7 @@ void RenderingManager::rebuildPtr(const MWWorld::Ptr &ptr) anim->rebuild(); if(mCamera->getHandle() == ptr.getRefData().getHandle()) { - mCamera->attachTo(ptr); + attachCameraTo(ptr); mCamera->setAnimation(anim); } } @@ -337,10 +346,13 @@ void RenderingManager::update (float duration, bool paused) MWWorld::Ptr player = world->getPlayerPtr(); - int blind = player.getClass().getCreatureStats(player).getMagicEffects().get(ESM::MagicEffect::Blind).mMagnitude; - mRendering.getFader()->setFactor(std::max(0.f, 1.f-(blind / 100.f))); + int blind = player.getClass().getCreatureStats(player).getMagicEffects().get(ESM::MagicEffect::Blind).getMagnitude(); + MWBase::Environment::get().getWindowManager()->setBlindness(std::max(0, std::min(100, blind))); setAmbientMode(); + if (player.getClass().getNpcStats(player).isWerewolf()) + MWBase::Environment::get().getWindowManager()->setWerewolfOverlay(mCamera->isFirstPerson()); + // player position MWWorld::RefData &data = player.getRefData(); Ogre::Vector3 playerPos(data.getPosition().pos); @@ -415,18 +427,12 @@ void RenderingManager::postRenderTargetUpdate(const RenderTargetEvent &evt) mOcclusionQuery->setActive(false); } -void RenderingManager::waterAdded (MWWorld::CellStore *store) +void RenderingManager::setWaterEnabled(bool enable) { - if (store->getCell()->mData.mFlags & ESM::Cell::HasWater) - { - mWater->changeCell (store->getCell()); - mWater->setActive(true); - } - else - removeWater(); + mWater->setActive(enable); } -void RenderingManager::setWaterHeight(const float height) +void RenderingManager::setWaterHeight(float height) { mWater->setHeight(height); } @@ -493,7 +499,7 @@ bool RenderingManager::toggleRenderMode(int mode) } } -void RenderingManager::configureFog(MWWorld::CellStore &mCell) +void RenderingManager::configureFog(const MWWorld::CellStore &mCell) { Ogre::ColourValue color; color.setAsABGR (mCell.getCell()->mAmbi.mFog); @@ -504,12 +510,21 @@ void RenderingManager::configureFog(MWWorld::CellStore &mCell) void RenderingManager::configureFog(const float density, const Ogre::ColourValue& colour) { mFogColour = colour; - float max = Settings::Manager::getFloat("max viewing distance", "Viewing distance"); + float max = Settings::Manager::getFloat("viewing distance", "Viewing distance"); - mFogStart = max / (density) * Settings::Manager::getFloat("fog start factor", "Viewing distance"); - mFogEnd = max / (density) * Settings::Manager::getFloat("fog end factor", "Viewing distance"); + if (density == 0) + { + mFogStart = 0; + mFogEnd = std::numeric_limits::max(); + mRendering.getCamera()->setFarClipDistance (max); + } + else + { + mFogStart = max / (density) * Settings::Manager::getFloat("fog start factor", "Viewing distance"); + mFogEnd = max / (density) * Settings::Manager::getFloat("fog end factor", "Viewing distance"); + mRendering.getCamera()->setFarClipDistance (max / density); + } - mRendering.getCamera()->setFarClipDistance ( Settings::Manager::getFloat("max viewing distance", "Viewing distance") / density ); } void RenderingManager::applyFog (bool underwater) @@ -522,9 +537,10 @@ void RenderingManager::applyFog (bool underwater) } else { - mRendering.getScene()->setFog (FOG_LINEAR, Ogre::ColourValue(0.18039, 0.23137, 0.25490), 0, 0, 1000); - mRendering.getViewport()->setBackgroundColour (Ogre::ColourValue(0.18039, 0.23137, 0.25490)); - mWater->setViewportBackground (Ogre::ColourValue(0.18039, 0.23137, 0.25490)); + Ogre::ColourValue clv(0.090195, 0.115685, 0.12745); + mRendering.getScene()->setFog (FOG_LINEAR, Ogre::ColourValue(clv), 0, 0, 1000); + mRendering.getViewport()->setBackgroundColour (Ogre::ColourValue(clv)); + mWater->setViewportBackground (Ogre::ColourValue(clv)); } } @@ -568,24 +584,6 @@ void RenderingManager::configureAmbient(MWWorld::CellStore &mCell) sunEnable(false); } } -// Switch through lighting modes. - -void RenderingManager::toggleLight() -{ - if (mAmbientMode==2) - mAmbientMode = 0; - else - ++mAmbientMode; - - switch (mAmbientMode) - { - case 0: std::cout << "Setting lights to normal\n"; break; - case 1: std::cout << "Turning the lights up\n"; break; - case 2: std::cout << "Turning the lights to full\n"; break; - } - - setAmbientMode(); -} void RenderingManager::setSunColour(const Ogre::ColourValue& colour) { @@ -599,7 +597,7 @@ void RenderingManager::setAmbientColour(const Ogre::ColourValue& colour) mAmbientColor = colour; MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); - int nightEye = player.getClass().getCreatureStats(player).getMagicEffects().get(ESM::MagicEffect::NightEye).mMagnitude; + int nightEye = player.getClass().getCreatureStats(player).getMagicEffects().get(ESM::MagicEffect::NightEye).getMagnitude(); Ogre::ColourValue final = colour; final += Ogre::ColourValue(0.7,0.7,0.7,0) * std::min(1.f, (nightEye/100.f)); @@ -631,12 +629,12 @@ void RenderingManager::sunDisable(bool real) } } -void RenderingManager::setSunDirection(const Ogre::Vector3& direction) +void RenderingManager::setSunDirection(const Ogre::Vector3& direction, bool is_moon) { // direction * -1 (because 'direction' is camera to sun vector and not sun to camera), if (mSun) mSun->setDirection(Vector3(-direction.x, -direction.y, -direction.z)); - mSkyManager->setSunDirection(direction); + mSkyManager->setSunDirection(direction, is_moon); } void RenderingManager::setGlare(bool glare) @@ -689,11 +687,6 @@ void RenderingManager::enableLights(bool sun) sunEnable(sun); } -Shadows* RenderingManager::getShadows() -{ - return mShadows; -} - void RenderingManager::notifyWorldSpaceChanged() { mEffectManager->clear(); @@ -749,9 +742,10 @@ void RenderingManager::processChangedSettings(const Settings::CategorySettingVec { setMenuTransparency(Settings::Manager::getFloat("menu transparency", "GUI")); } - else if (it->second == "max viewing distance" && it->first == "Viewing distance") + else if (it->second == "viewing distance" && it->first == "Viewing distance") { - if (!MWBase::Environment::get().getWorld()->isCellExterior() && !MWBase::Environment::get().getWorld()->isCellQuasiExterior()) + if (!MWBase::Environment::get().getWorld()->isCellExterior() && !MWBase::Environment::get().getWorld()->isCellQuasiExterior() + && MWBase::Environment::get().getWorld()->getPlayerPtr().mCell) configureFog(*MWBase::Environment::get().getWorld()->getPlayerPtr().getCell()); } else if (it->first == "Video" && ( @@ -759,13 +753,6 @@ void RenderingManager::processChangedSettings(const Settings::CategorySettingVec || it->second == "resolution y" || it->second == "fullscreen")) changeRes = true; - else if (it->first == "Video" && it->second == "vsync") - { - // setVSyncEnabled is bugged in 1.8 -#if OGRE_VERSION >= (1 << 16 | 9 << 8 | 0) - mRendering.getWindow()->setVSyncEnabled(Settings::Manager::getBool("vsync", "Video")); -#endif - } else if (it->second == "field of view" && it->first == "General") mRendering.setFov(Settings::Manager::getFloat("field of view", "General")); else if ((it->second == "texture filtering" && it->first == "General") @@ -857,10 +844,9 @@ void RenderingManager::processChangedSettings(const Settings::CategorySettingVec void RenderingManager::setMenuTransparency(float val) { - Ogre::TexturePtr tex = Ogre::TextureManager::getSingleton().getByName("transparent.png"); - std::vector buffer; + Ogre::TexturePtr tex = Ogre::TextureManager::getSingleton().getByName("transparent.png"); std::vector buffer; buffer.resize(1); - buffer[0] = (int(255*val) << 24); + buffer[0] = (int(255*val) << 24) | (255 << 16) | (255 << 8) | 255; memcpy(tex->getBuffer()->lock(Ogre::HardwareBuffer::HBL_DISCARD), &buffer[0], 1*4); tex->getBuffer()->unlock(); } @@ -883,7 +869,13 @@ void RenderingManager::getTriangleBatchCount(unsigned int &triangles, unsigned i void RenderingManager::setupPlayer(const MWWorld::Ptr &ptr) { ptr.getRefData().setBaseNode(mRendering.getScene()->getSceneNode("player")); - mCamera->attachTo(ptr); + attachCameraTo(ptr); +} + +void RenderingManager::attachCameraTo(const MWWorld::Ptr &ptr) +{ + Ogre::SceneNode* cameraNode = mCamera->attachTo(ptr); + mSkyManager->attachToNode(cameraNode); } void RenderingManager::renderPlayer(const MWWorld::Ptr &ptr) @@ -932,9 +924,14 @@ void RenderingManager::setCameraDistance(float dist, bool adjust, bool override) } } -void RenderingManager::getInteriorMapPosition (Ogre::Vector2 position, float& nX, float& nY, int &x, int& y) +void RenderingManager::worldToInteriorMapPosition (Ogre::Vector2 position, float& nX, float& nY, int &x, int& y) { - return mLocalMap->getInteriorMapPosition (position, nX, nY, x, y); + return mLocalMap->worldToInteriorMapPosition (position, nX, nY, x, y); +} + +Ogre::Vector2 RenderingManager::interiorMapToWorldPosition(float nX, float nY, int x, int y) +{ + return mLocalMap->interiorMapToWorldPosition(nX, nY, x, y); } bool RenderingManager::isPositionExplored (float nX, float nY, int x, int y, bool interior) @@ -1028,9 +1025,12 @@ void RenderingManager::enableTerrain(bool enable) { if (!mTerrain) { - mTerrain = new Terrain::World(mRendering.getScene(), new MWRender::TerrainStorage(), RV_Terrain, - Settings::Manager::getBool("distant land", "Terrain"), - Settings::Manager::getBool("shader", "Terrain"), Terrain::Align_XY, 1, 64); + if (Settings::Manager::getBool("distant land", "Terrain")) + mTerrain = new Terrain::DefaultWorld(mRendering.getScene(), new MWRender::TerrainStorage(), RV_Terrain, + Settings::Manager::getBool("shader", "Terrain"), Terrain::Align_XY, 1, 64); + else + mTerrain = new Terrain::TerrainGrid(mRendering.getScene(), new MWRender::TerrainStorage(), RV_Terrain, + Settings::Manager::getBool("shader", "Terrain"), Terrain::Align_XY); mTerrain->applyMaterials(Settings::Manager::getBool("enabled", "Shadows"), Settings::Manager::getBool("split", "Shadows")); mTerrain->update(mRendering.getCamera()->getRealPosition()); diff --git a/apps/openmw/mwrender/renderingmanager.hpp b/apps/openmw/mwrender/renderingmanager.hpp index f539f9270..c3eedce7b 100644 --- a/apps/openmw/mwrender/renderingmanager.hpp +++ b/apps/openmw/mwrender/renderingmanager.hpp @@ -4,8 +4,6 @@ #include "sky.hpp" #include "debugging.hpp" -#include - #include #include @@ -95,17 +93,13 @@ public: MWRender::Camera* getCamera() const; - void toggleLight(); bool toggleRenderMode(int mode); - OEngine::Render::Fader* getFader(); - void removeCell (MWWorld::CellStore *store); /// \todo this function should be removed later. Instead the rendering subsystems should track /// when rebatching is needed and update automatically at the end of each frame. void cellAdded (MWWorld::CellStore *store); - void waterAdded(MWWorld::CellStore *store); /// Clear all savegame-specific data (i.e. fog of war textures) void clear(); @@ -126,8 +120,10 @@ public: /// Updates an object's rotation void rotateObject (const MWWorld::Ptr& ptr); - void setWaterHeight(const float height); + void setWaterHeight(float height); + void setWaterEnabled(bool enabled); bool toggleWater(); + bool toggleWorld(); /// Updates object rendering after cell change /// \param old Object reference in previous cell @@ -145,7 +141,7 @@ public: void setAmbientColour(const Ogre::ColourValue& colour); void setSunColour(const Ogre::ColourValue& colour); - void setSunDirection(const Ogre::Vector3& direction); + void setSunDirection(const Ogre::Vector3& direction, bool is_moon); void sunEnable(bool real); ///< @param real whether or not to really disable the sunlight (otherwise just set diffuse to 0) void sunDisable(bool real); @@ -161,8 +157,6 @@ public: float getTerrainHeightAt (Ogre::Vector3 worldPos); - Shadows* getShadows(); - void notifyWorldSpaceChanged(); void getTriangleBatchCount(unsigned int &triangles, unsigned int &batches); @@ -189,7 +183,7 @@ public: ///< request the local map for a cell /// configure fog according to cell - void configureFog(MWWorld::CellStore &mCell); + void configureFog(const MWWorld::CellStore &mCell); /// configure fog manually void configureFog(const float density, const Ogre::ColourValue& colour); @@ -202,8 +196,11 @@ public: Ogre::Viewport* getViewport() { return mRendering.getViewport(); } - void getInteriorMapPosition (Ogre::Vector2 position, float& nX, float& nY, int &x, int& y); - ///< see MWRender::LocalMap::getInteriorMapPosition + void worldToInteriorMapPosition (Ogre::Vector2 position, float& nX, float& nY, int &x, int& y); + ///< see MWRender::LocalMap::worldToInteriorMapPosition + + Ogre::Vector2 interiorMapToWorldPosition (float nX, float nY, int x, int y); + ///< see MWRender::LocalMap::interiorMapToWorldPosition bool isPositionExplored (float nX, float nY, int x, int y, bool interior); ///< see MWRender::LocalMap::isPositionExplored @@ -224,6 +221,8 @@ private: void setAmbientMode(); void applyFog(bool underwater); + void attachCameraTo(const MWWorld::Ptr& ptr); + void setMenuTransparency(float val); bool mSunEnabled; @@ -270,6 +269,8 @@ private: MWRender::LocalMap* mLocalMap; MWRender::Shadows* mShadows; + + bool mRenderWorld; }; } diff --git a/apps/openmw/mwrender/ripplesimulation.cpp b/apps/openmw/mwrender/ripplesimulation.cpp index 74216c1de..64b5e48c3 100644 --- a/apps/openmw/mwrender/ripplesimulation.cpp +++ b/apps/openmw/mwrender/ripplesimulation.cpp @@ -165,7 +165,7 @@ void RippleSimulation::addImpulses() // for non-player actors this is done in updateObjectCell it->mPtr = MWBase::Environment::get().getWorld ()->getPlayerPtr(); } - float* _currentPos = it->mPtr.getRefData().getPosition().pos; + const float* _currentPos = it->mPtr.getRefData().getPosition().pos; Ogre::Vector3 currentPos (_currentPos[0], _currentPos[1], _currentPos[2]); if ( (currentPos - it->mLastEmitPosition).length() > 2 diff --git a/apps/openmw/mwrender/ripplesimulation.hpp b/apps/openmw/mwrender/ripplesimulation.hpp index 7e7eebc1c..e203212cf 100644 --- a/apps/openmw/mwrender/ripplesimulation.hpp +++ b/apps/openmw/mwrender/ripplesimulation.hpp @@ -41,6 +41,9 @@ public: void updateEmitterPtr (const MWWorld::Ptr& old, const MWWorld::Ptr& ptr); private: + RippleSimulation(const RippleSimulation&); + RippleSimulation& operator=(const RippleSimulation&); + std::vector mEmitters; Ogre::RenderTexture* mRenderTargets[4]; diff --git a/apps/openmw/mwrender/shadows.cpp b/apps/openmw/mwrender/shadows.cpp index 33e337649..5a6ccaca6 100644 --- a/apps/openmw/mwrender/shadows.cpp +++ b/apps/openmw/mwrender/shadows.cpp @@ -20,10 +20,9 @@ using namespace Ogre; using namespace MWRender; Shadows::Shadows(OEngine::Render::OgreRenderer* rend) : + mRendering(rend), mSceneMgr(rend->getScene()), mPSSMSetup(NULL), mShadowFar(1000), mFadeStart(0.9) { - mRendering = rend; - mSceneMgr = mRendering->getScene(); recreate(); } @@ -186,13 +185,3 @@ PSSMShadowCameraSetup* Shadows::getPSSMSetup() { return mPSSMSetup; } - -float Shadows::getShadowFar() const -{ - return mShadowFar; -} - -float Shadows::getFadeStart() const -{ - return mFadeStart; -} diff --git a/apps/openmw/mwrender/shadows.hpp b/apps/openmw/mwrender/shadows.hpp index bc2b141f7..fe125f54c 100644 --- a/apps/openmw/mwrender/shadows.hpp +++ b/apps/openmw/mwrender/shadows.hpp @@ -23,8 +23,6 @@ namespace MWRender void recreate(); Ogre::PSSMShadowCameraSetup* getPSSMSetup(); - float getShadowFar() const; - float getFadeStart() const; protected: OEngine::Render::OgreRenderer* mRendering; diff --git a/apps/openmw/mwrender/sky.cpp b/apps/openmw/mwrender/sky.cpp index 90c08c299..ccef74efb 100644 --- a/apps/openmw/mwrender/sky.cpp +++ b/apps/openmw/mwrender/sky.cpp @@ -8,16 +8,18 @@ #include #include #include -#include +#include #include #include #include +#include #include #include #include +#include #include @@ -67,6 +69,7 @@ BillboardObject::BillboardObject( const String& textureName, } BillboardObject::BillboardObject() +: mNode(NULL), mMaterial(NULL), mEntity(NULL) { } @@ -186,11 +189,6 @@ void Moon::setPhase(const Moon::Phase& phase) mPhase = phase; } -Moon::Phase Moon::getPhase() const -{ - return mPhase; -} - unsigned int Moon::getPhaseInt() const { if (mPhase == Moon::Phase_New) return 0; @@ -218,12 +216,14 @@ SkyManager::SkyManager(Ogre::SceneNode *root, Ogre::Camera *pCamera) , mSceneMgr(NULL) , mAtmosphereDay(NULL) , mAtmosphereNight(NULL) + , mCloudNode(NULL) , mClouds() , mNextClouds() , mCloudBlendFactor(0.0f) , mCloudOpacity(0.0f) , mCloudSpeed(0.0f) , mStarsOpacity(0.0f) + , mLightning(NULL) , mRemainingTransitionTime(0.0f) , mGlareFade(0.0f) , mGlare(0.0f) @@ -234,10 +234,16 @@ SkyManager::SkyManager(Ogre::SceneNode *root, Ogre::Camera *pCamera) , mCreated(false) , mCloudAnimationTimer(0.f) , mMoonRed(false) + , mParticleNode(NULL) + , mRainEnabled(false) + , mRainTimer(0) + , mRainSpeed(0) + , mRainFrequency(1) + , mStormDirection(0,-1,0) + , mIsStorm(false) { mSceneMgr = root->getCreator(); mRootNode = mSceneMgr->getRootSceneNode()->createChildSceneNode(); - mRootNode->setInheritOrientation(false); } void SkyManager::create() @@ -286,7 +292,12 @@ void SkyManager::create() // Stars mAtmosphereNight = mRootNode->createChildSceneNode(); - NifOgre::ObjectScenePtr objects = NifOgre::Loader::createObjects(mAtmosphereNight, "meshes\\sky_night_01.nif"); + NifOgre::ObjectScenePtr objects; + if (Ogre::ResourceGroupManager::getSingleton().resourceExistsInAnyGroup("meshes\\sky_night_02.nif")) + objects = NifOgre::Loader::createObjects(mAtmosphereNight, "meshes\\sky_night_02.nif"); + else + objects = NifOgre::Loader::createObjects(mAtmosphereNight, "meshes\\sky_night_01.nif"); + for(size_t i = 0, matidx = 0;i < objects->mEntities.size();i++) { Entity* night1_ent = objects->mEntities[i]; @@ -329,8 +340,8 @@ void SkyManager::create() mObjects.push_back(objects); // Clouds - SceneNode* clouds_node = mRootNode->createChildSceneNode(); - objects = NifOgre::Loader::createObjects(clouds_node, "meshes\\sky_clouds_01.nif"); + mCloudNode = mRootNode->createChildSceneNode(); + objects = NifOgre::Loader::createObjects(mCloudNode, "meshes\\sky_clouds_01.nif"); for(size_t i = 0;i < objects->mEntities.size();i++) { Entity* clouds_ent = objects->mEntities[i]; @@ -349,6 +360,7 @@ void SkyManager::create() SkyManager::~SkyManager() { + clearRain(); delete mSun; delete mSunGlare; delete mMasser; @@ -367,11 +379,96 @@ int SkyManager::getSecundaPhase() const return mSecunda->getPhaseInt(); } +void SkyManager::clearRain() +{ + for (std::map::iterator it = mRainModels.begin(); it != mRainModels.end();) + { + it->second.setNull(); + mSceneMgr->destroySceneNode(it->first); + mRainModels.erase(it++); + } +} + +void SkyManager::updateRain(float dt) +{ + // Move existing rain + // Note: if rain gets disabled, we let the existing rain drops finish falling down. + float minHeight = 200; + for (std::map::iterator it = mRainModels.begin(); it != mRainModels.end();) + { + Ogre::Vector3 pos = it->first->getPosition(); + pos.z -= mRainSpeed * dt; + it->first->setPosition(pos); + if (pos.z < -minHeight) + { + it->second.setNull(); + mSceneMgr->destroySceneNode(it->first); + mRainModels.erase(it++); + } + else + ++it; + } + + // Spawn new rain + float rainFrequency = mRainFrequency; + if (mRainEnabled) + { + mRainTimer += dt; + if (mRainTimer >= 1.f/rainFrequency) + { + mRainTimer = 0; + + // TODO: handle rain settings from Morrowind.ini + const float rangeRandom = 100; + float xOffs = (std::rand()/(RAND_MAX+1.0)) * rangeRandom - (rangeRandom/2); + float yOffs = (std::rand()/(RAND_MAX+1.0)) * rangeRandom - (rangeRandom/2); + + // Create a separate node to control the offset, since a node with setInheritOrientation(false) will still + // consider the orientation of the parent node for its position, just not for its orientation + float startHeight = 700; + Ogre::SceneNode* offsetNode = mParticleNode->createChildSceneNode(Ogre::Vector3(xOffs,yOffs,startHeight)); + + // Spawn a new rain object for each instance. + // TODO: this is inefficient. We could try to use an Ogre::ParticleSystem instead, but then we would need to make assumptions + // about the rain meshes being Quads and their dimensions. + // Or we could clone meshes into one vertex buffer manually. + NifOgre::ObjectScenePtr objects = NifOgre::Loader::createObjects(offsetNode, mRainEffect); + for (unsigned int i=0; imEntities.size(); ++i) + { + objects->mEntities[i]->setRenderQueueGroup(RQG_Alpha); + objects->mEntities[i]->setVisibilityFlags(RV_Sky); + } + for (unsigned int i=0; imParticles.size(); ++i) + { + objects->mParticles[i]->setRenderQueueGroup(RQG_Alpha); + objects->mParticles[i]->setVisibilityFlags(RV_Sky); + } + mRainModels[offsetNode] = objects; + } + } +} + void SkyManager::update(float duration) { if (!mEnabled) return; const MWWorld::Fallback* fallback=MWBase::Environment::get().getWorld()->getFallback(); + if (!mParticle.isNull()) + { + for (unsigned int i=0; imControllers.size(); ++i) + mParticle->mControllers[i].update(); + + if (mIsStorm) + mParticleNode->setOrientation(Ogre::Vector3::UNIT_Y.getRotationTo(mStormDirection)); + } + + if (mIsStorm) + mCloudNode->setOrientation(Ogre::Vector3::UNIT_Y.getRotationTo(mStormDirection)); + else + mCloudNode->setOrientation(Ogre::Quaternion::IDENTITY); + + updateRain(duration); + // UV Scroll the clouds mCloudAnimationTimer += duration * mCloudSpeed; sh::Factory::getInstance().setSharedParameter ("cloudAnimationTimer", @@ -422,13 +519,22 @@ void SkyManager::enable() if (!mCreated) create(); + if (mParticleNode) + mParticleNode->setVisible(true); + mRootNode->setVisible(true); mEnabled = true; } void SkyManager::disable() { + if (mParticleNode) + mParticleNode->setVisible(false); + + clearRain(); + mRootNode->setVisible(false); + mEnabled = false; } @@ -441,15 +547,46 @@ void SkyManager::setWeather(const MWWorld::WeatherResult& weather) { if (!mCreated) return; + mRainEffect = weather.mRainEffect; + mRainEnabled = !mRainEffect.empty(); + mRainFrequency = weather.mRainFrequency; + mRainSpeed = weather.mRainSpeed; + mIsStorm = weather.mIsStorm; + + if (mCurrentParticleEffect != weather.mParticleEffect) + { + mCurrentParticleEffect = weather.mParticleEffect; + + if (mCurrentParticleEffect.empty()) + { + mParticle.setNull(); + } + else + { + mParticle = NifOgre::Loader::createObjects(mParticleNode, mCurrentParticleEffect); + for(size_t i = 0; i < mParticle->mParticles.size(); ++i) + { + ParticleSystem* particle = mParticle->mParticles[i]; + particle->setRenderQueueGroup(RQG_Alpha); + particle->setVisibilityFlags(RV_Sky); + } + for (size_t i = 0; i < mParticle->mControllers.size(); ++i) + { + if (mParticle->mControllers[i].getSource().isNull()) + mParticle->mControllers[i].setSource(Ogre::ControllerManager::getSingleton().getFrameTimeSource()); + } + } + } + if (mClouds != weather.mCloudTexture) { - sh::Factory::getInstance().setTextureAlias ("cloud_texture_1", "textures\\"+weather.mCloudTexture); + sh::Factory::getInstance().setTextureAlias ("cloud_texture_1", Misc::ResourceHelpers::correctTexturePath(weather.mCloudTexture)); mClouds = weather.mCloudTexture; } if (mNextClouds != weather.mNextCloudTexture) { - sh::Factory::getInstance().setTextureAlias ("cloud_texture_2", "textures\\"+weather.mNextCloudTexture); + sh::Factory::getInstance().setTextureAlias ("cloud_texture_2", Misc::ResourceHelpers::correctTexturePath(weather.mNextCloudTexture)); mNextClouds = weather.mNextCloudTexture; } @@ -546,14 +683,19 @@ void SkyManager::sunDisable() mSunEnabled = false; } -void SkyManager::setSunDirection(const Vector3& direction) +void SkyManager::setStormDirection(const Vector3 &direction) +{ + mStormDirection = direction; +} + +void SkyManager::setSunDirection(const Vector3& direction, bool is_moon) { if (!mCreated) return; mSun->setPosition(direction); mSunGlare->setPosition(direction); float height = direction.z; - float fade = ( height > 0.5) ? 1.0 : height * 2; + float fade = is_moon ? 0.0 : (( height > 0.5) ? 1.0 : height * 2); sh::Factory::getInstance ().setSharedParameter ("waterSunFade_sunHeight", sh::makeProperty(new sh::Vector2(fade, height))); } @@ -600,16 +742,6 @@ void SkyManager::setLightningStrength(const float factor) else mLightning->setVisible(false); } -void SkyManager::setLightningEnabled(bool enabled) -{ - /// \todo -} - -void SkyManager::setLightningDirection(const Ogre::Vector3& dir) -{ - if (!mCreated) return; - mLightning->setDirection (dir); -} void SkyManager::setMasserFade(const float fade) { @@ -646,3 +778,16 @@ void SkyManager::setGlareEnabled (bool enabled) return; mSunGlare->setVisible (mSunEnabled && enabled); } + +void SkyManager::attachToNode(SceneNode *sceneNode) +{ + if (!mParticleNode) + { + mParticleNode = sceneNode->createChildSceneNode(); + mParticleNode->setInheritOrientation(false); + } + else + { + sceneNode->addChild(mParticleNode); + } +} diff --git a/apps/openmw/mwrender/sky.hpp b/apps/openmw/mwrender/sky.hpp index 965907a97..0544f17ef 100644 --- a/apps/openmw/mwrender/sky.hpp +++ b/apps/openmw/mwrender/sky.hpp @@ -103,7 +103,6 @@ namespace MWRender void setPhase(const Phase& phase); void setType(const Type& type); - Phase getPhase() const; unsigned int getPhaseInt() const; private: @@ -117,6 +116,9 @@ namespace MWRender SkyManager(Ogre::SceneNode* root, Ogre::Camera* pCamera); ~SkyManager(); + /// Attach weather particle effects to this scene node (should be the Camera's parent node) + void attachToNode(Ogre::SceneNode* sceneNode); + void update(float duration); void enable(); @@ -148,7 +150,11 @@ namespace MWRender void sunDisable(); - void setSunDirection(const Ogre::Vector3& direction); + void setRainSpeed(float speed); + + void setStormDirection(const Ogre::Vector3& direction); + + void setSunDirection(const Ogre::Vector3& direction, bool is_moon); void setMasserDirection(const Ogre::Vector3& direction); @@ -165,8 +171,6 @@ namespace MWRender void secundaDisable(); void setLightningStrength(const float factor); - void setLightningDirection(const Ogre::Vector3& dir); - void setLightningEnabled(bool enabled); ///< disable prior to map render void setGlare(const float glare); void setGlareEnabled(bool enabled); @@ -176,10 +180,15 @@ namespace MWRender void create(); ///< no need to call this, automatically done on first enable() + void updateRain(float dt); + void clearRain(); + bool mCreated; bool mMoonRed; + bool mIsStorm; + float mHour; int mDay; int mMonth; @@ -198,8 +207,18 @@ namespace MWRender Ogre::SceneNode* mAtmosphereDay; Ogre::SceneNode* mAtmosphereNight; + Ogre::SceneNode* mCloudNode; + std::vector mObjects; + Ogre::SceneNode* mParticleNode; + NifOgre::ObjectScenePtr mParticle; + + std::map mRainModels; + float mRainTimer; + + Ogre::Vector3 mStormDirection; + // remember some settings so we don't have to apply them again if they didnt change Ogre::String mClouds; Ogre::String mNextClouds; @@ -211,6 +230,8 @@ namespace MWRender Ogre::ColourValue mSkyColour; Ogre::ColourValue mFogColour; + std::string mCurrentParticleEffect; + Ogre::Light* mLightning; float mRemainingTransitionTime; @@ -218,6 +239,11 @@ namespace MWRender float mGlare; // target float mGlareFade; // actual + bool mRainEnabled; + std::string mRainEffect; + float mRainSpeed; + float mRainFrequency; + bool mEnabled; bool mSunEnabled; bool mMasserEnabled; diff --git a/apps/openmw/mwrender/terrainstorage.cpp b/apps/openmw/mwrender/terrainstorage.cpp index 2558c95c5..cbd9e2444 100644 --- a/apps/openmw/mwrender/terrainstorage.cpp +++ b/apps/openmw/mwrender/terrainstorage.cpp @@ -1,21 +1,11 @@ #include "terrainstorage.hpp" -#include -#include -#include -#include -#include -#include -#include - #include #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" #include "../mwworld/esmstore.hpp" -#include - namespace MWRender { @@ -59,515 +49,4 @@ namespace MWRender return esmStore.get().find(index, plugin); } - bool TerrainStorage::getMinMaxHeights(float size, const Ogre::Vector2 ¢er, float &min, float &max) - { - assert (size <= 1 && "TerrainStorage::getMinMaxHeights, chunk size should be <= 1 cell"); - - /// \todo investigate if min/max heights should be stored at load time in ESM::Land instead - - Ogre::Vector2 origin = center - Ogre::Vector2(size/2.f, size/2.f); - - assert(origin.x == (int) origin.x); - assert(origin.y == (int) origin.y); - - int cellX = origin.x; - int cellY = origin.y; - - const ESM::Land* land = getLand(cellX, cellY); - if (!land) - return false; - - min = std::numeric_limits().max(); - max = -std::numeric_limits().max(); - for (int row=0; rowmLandData->mHeights[col*ESM::Land::LAND_SIZE+row]; - if (h > max) - max = h; - if (h < min) - min = h; - } - } - return true; - } - - void TerrainStorage::fixNormal (Ogre::Vector3& normal, int cellX, int cellY, int col, int row) - { - while (col >= ESM::Land::LAND_SIZE-1) - { - ++cellY; - col -= ESM::Land::LAND_SIZE-1; - } - while (row >= ESM::Land::LAND_SIZE-1) - { - ++cellX; - row -= ESM::Land::LAND_SIZE-1; - } - while (col < 0) - { - --cellY; - col += ESM::Land::LAND_SIZE-1; - } - while (row < 0) - { - --cellX; - row += ESM::Land::LAND_SIZE-1; - } - ESM::Land* land = getLand(cellX, cellY); - if (land && land->mHasData) - { - normal.x = land->mLandData->mNormals[col*ESM::Land::LAND_SIZE*3+row*3]; - normal.y = land->mLandData->mNormals[col*ESM::Land::LAND_SIZE*3+row*3+1]; - normal.z = land->mLandData->mNormals[col*ESM::Land::LAND_SIZE*3+row*3+2]; - normal.normalise(); - } - else - normal = Ogre::Vector3(0,0,1); - } - - void TerrainStorage::averageNormal(Ogre::Vector3 &normal, int cellX, int cellY, int col, int row) - { - Ogre::Vector3 n1,n2,n3,n4; - fixNormal(n1, cellX, cellY, col+1, row); - fixNormal(n2, cellX, cellY, col-1, row); - fixNormal(n3, cellX, cellY, col, row+1); - fixNormal(n4, cellX, cellY, col, row-1); - normal = (n1+n2+n3+n4); - normal.normalise(); - } - - void TerrainStorage::fixColour (Ogre::ColourValue& color, int cellX, int cellY, int col, int row) - { - if (col == ESM::Land::LAND_SIZE-1) - { - ++cellY; - col = 0; - } - if (row == ESM::Land::LAND_SIZE-1) - { - ++cellX; - row = 0; - } - ESM::Land* land = getLand(cellX, cellY); - if (land && land->mLandData->mUsingColours) - { - color.r = land->mLandData->mColours[col*ESM::Land::LAND_SIZE*3+row*3] / 255.f; - color.g = land->mLandData->mColours[col*ESM::Land::LAND_SIZE*3+row*3+1] / 255.f; - color.b = land->mLandData->mColours[col*ESM::Land::LAND_SIZE*3+row*3+2] / 255.f; - } - else - { - color.r = 1; - color.g = 1; - color.b = 1; - } - - } - - void TerrainStorage::fillVertexBuffers (int lodLevel, float size, const Ogre::Vector2& center, Terrain::Alignment align, - std::vector& positions, - std::vector& normals, - std::vector& colours) - { - // LOD level n means every 2^n-th vertex is kept - size_t increment = 1 << lodLevel; - - Ogre::Vector2 origin = center - Ogre::Vector2(size/2.f, size/2.f); - assert(origin.x == (int) origin.x); - assert(origin.y == (int) origin.y); - - int startX = origin.x; - int startY = origin.y; - - size_t numVerts = size*(ESM::Land::LAND_SIZE-1)/increment + 1; - - colours.resize(numVerts*numVerts*4); - positions.resize(numVerts*numVerts*3); - normals.resize(numVerts*numVerts*3); - - Ogre::Vector3 normal; - Ogre::ColourValue color; - - float vertY; - float vertX; - - float vertY_ = 0; // of current cell corner - for (int cellY = startY; cellY < startY + std::ceil(size); ++cellY) - { - float vertX_ = 0; // of current cell corner - for (int cellX = startX; cellX < startX + std::ceil(size); ++cellX) - { - ESM::Land* land = getLand(cellX, cellY); - if (land && !land->mHasData) - land = NULL; - bool hasColors = land && land->mLandData->mUsingColours; - - int rowStart = 0; - int colStart = 0; - // Skip the first row / column unless we're at a chunk edge, - // since this row / column is already contained in a previous cell - if (colStart == 0 && vertY_ != 0) - colStart += increment; - if (rowStart == 0 && vertX_ != 0) - rowStart += increment; - - vertY = vertY_; - for (int col=colStart; colmLandData->mHeights[col*ESM::Land::LAND_SIZE+row]; - else - positions[vertX*numVerts*3 + vertY*3 + 2] = -2048; - - if (land) - { - normal.x = land->mLandData->mNormals[col*ESM::Land::LAND_SIZE*3+row*3]; - normal.y = land->mLandData->mNormals[col*ESM::Land::LAND_SIZE*3+row*3+1]; - normal.z = land->mLandData->mNormals[col*ESM::Land::LAND_SIZE*3+row*3+2]; - normal.normalise(); - } - else - normal = Ogre::Vector3(0,0,1); - - // Normals apparently don't connect seamlessly between cells - if (col == ESM::Land::LAND_SIZE-1 || row == ESM::Land::LAND_SIZE-1) - fixNormal(normal, cellX, cellY, col, row); - - // some corner normals appear to be complete garbage (z < 0) - if ((row == 0 || row == ESM::Land::LAND_SIZE-1) && (col == 0 || col == ESM::Land::LAND_SIZE-1)) - averageNormal(normal, cellX, cellY, col, row); - - assert(normal.z > 0); - - normals[vertX*numVerts*3 + vertY*3] = normal.x; - normals[vertX*numVerts*3 + vertY*3 + 1] = normal.y; - normals[vertX*numVerts*3 + vertY*3 + 2] = normal.z; - - if (hasColors) - { - color.r = land->mLandData->mColours[col*ESM::Land::LAND_SIZE*3+row*3] / 255.f; - color.g = land->mLandData->mColours[col*ESM::Land::LAND_SIZE*3+row*3+1] / 255.f; - color.b = land->mLandData->mColours[col*ESM::Land::LAND_SIZE*3+row*3+2] / 255.f; - } - else - { - color.r = 1; - color.g = 1; - color.b = 1; - } - - // Unlike normals, colors mostly connect seamlessly between cells, but not always... - if (col == ESM::Land::LAND_SIZE-1 || row == ESM::Land::LAND_SIZE-1) - fixColour(color, cellX, cellY, col, row); - - color.a = 1; - Ogre::uint32 rsColor; - Ogre::Root::getSingleton().getRenderSystem()->convertColourValue(color, &rsColor); - memcpy(&colours[vertX*numVerts*4 + vertY*4], &rsColor, sizeof(Ogre::uint32)); - - ++vertX; - } - ++vertY; - } - vertX_ = vertX; - } - vertY_ = vertY; - - assert(vertX_ == numVerts); // Ensure we covered whole area - } - assert(vertY_ == numVerts); // Ensure we covered whole area - } - - TerrainStorage::UniqueTextureId TerrainStorage::getVtexIndexAt(int cellX, int cellY, - int x, int y) - { - // For the first/last row/column, we need to get the texture from the neighbour cell - // to get consistent blending at the borders - --x; - if (x < 0) - { - --cellX; - x += ESM::Land::LAND_TEXTURE_SIZE; - } - if (y >= ESM::Land::LAND_TEXTURE_SIZE) // Y appears to be wrapped from the other side because why the hell not? - { - ++cellY; - y -= ESM::Land::LAND_TEXTURE_SIZE; - } - - assert(xmLandData->mTextures[y * ESM::Land::LAND_TEXTURE_SIZE + x]; - if (tex == 0) - return std::make_pair(0,0); // vtex 0 is always the base texture, regardless of plugin - return std::make_pair(tex, land->mPlugin); - } - else - return std::make_pair(0,0); - } - - std::string TerrainStorage::getTextureName(UniqueTextureId id) - { - if (id.first == 0) - return "_land_default.dds"; // Not sure if the default texture floatly is hardcoded? - - // NB: All vtex ids are +1 compared to the ltex ids - const ESM::LandTexture* ltex = getLandTexture(id.first-1, id.second); - - std::string texture = ltex->mTexture; - //TODO this is needed due to MWs messed up texture handling - texture = texture.substr(0, texture.rfind(".")) + ".dds"; - - return texture; - } - - void TerrainStorage::getBlendmaps (const std::vector& nodes, std::vector& out, bool pack) - { - for (std::vector::const_iterator it = nodes.begin(); it != nodes.end(); ++it) - { - out.push_back(Terrain::LayerCollection()); - out.back().mTarget = *it; - getBlendmapsImpl((*it)->getSize(), (*it)->getCenter(), pack, out.back().mBlendmaps, out.back().mLayers); - } - } - - void TerrainStorage::getBlendmaps(float chunkSize, const Ogre::Vector2 &chunkCenter, - bool pack, std::vector &blendmaps, std::vector &layerList) - { - getBlendmapsImpl(chunkSize, chunkCenter, pack, blendmaps, layerList); - } - - void TerrainStorage::getBlendmapsImpl(float chunkSize, const Ogre::Vector2 &chunkCenter, - bool pack, std::vector &blendmaps, std::vector &layerList) - { - // TODO - blending isn't completely right yet; the blending radius appears to be - // different at a cell transition (2 vertices, not 4), so we may need to create a larger blendmap - // and interpolate the rest of the cell by hand? :/ - - Ogre::Vector2 origin = chunkCenter - Ogre::Vector2(chunkSize/2.f, chunkSize/2.f); - int cellX = origin.x; - int cellY = origin.y; - - // Save the used texture indices so we know the total number of textures - // and number of required blend maps - std::set textureIndices; - // Due to the way the blending works, the base layer will always shine through in between - // blend transitions (eg halfway between two texels, both blend values will be 0.5, so 25% of base layer visible). - // To get a consistent look, we need to make sure to use the same base layer in all cells. - // So we're always adding _land_default.dds as the base layer here, even if it's not referenced in this cell. - textureIndices.insert(std::make_pair(0,0)); - - for (int y=0; y textureIndicesMap; - for (std::set::iterator it = textureIndices.begin(); it != textureIndices.end(); ++it) - { - int size = textureIndicesMap.size(); - textureIndicesMap[*it] = size; - layerList.push_back(getLayerInfo(getTextureName(*it))); - } - - int numTextures = textureIndices.size(); - // numTextures-1 since the base layer doesn't need blending - int numBlendmaps = pack ? std::ceil((numTextures-1) / 4.f) : (numTextures-1); - - int channels = pack ? 4 : 1; - - // Second iteration - create and fill in the blend maps - const int blendmapSize = ESM::Land::LAND_TEXTURE_SIZE+1; - - for (int i=0; isecond; - int blendIndex = (pack ? std::floor((layerIndex-1)/4.f) : layerIndex-1); - int channel = pack ? std::max(0, (layerIndex-1) % 4) : 0; - - if (blendIndex == i) - pData[y*blendmapSize*channels + x*channels + channel] = 255; - else - pData[y*blendmapSize*channels + x*channels + channel] = 0; - } - } - blendmaps.push_back(Ogre::PixelBox(blendmapSize, blendmapSize, 1, format, pData)); - } - } - - float TerrainStorage::getHeightAt(const Ogre::Vector3 &worldPos) - { - int cellX = std::floor(worldPos.x / 8192.f); - int cellY = std::floor(worldPos.y / 8192.f); - - ESM::Land* land = getLand(cellX, cellY); - if (!land) - return -2048; - - // Mostly lifted from Ogre::Terrain::getHeightAtTerrainPosition - - // Normalized position in the cell - float nX = (worldPos.x - (cellX * 8192))/8192.f; - float nY = (worldPos.y - (cellY * 8192))/8192.f; - - // get left / bottom points (rounded down) - float factor = ESM::Land::LAND_SIZE - 1.0f; - float invFactor = 1.0f / factor; - - int startX = static_cast(nX * factor); - int startY = static_cast(nY * factor); - int endX = startX + 1; - int endY = startY + 1; - - assert(endX < ESM::Land::LAND_SIZE); - assert(endY < ESM::Land::LAND_SIZE); - - // now get points in terrain space (effectively rounding them to boundaries) - float startXTS = startX * invFactor; - float startYTS = startY * invFactor; - float endXTS = endX * invFactor; - float endYTS = endY * invFactor; - - // get parametric from start coord to next point - float xParam = (nX - startXTS) * factor; - float yParam = (nY - startYTS) * factor; - - /* For even / odd tri strip rows, triangles are this shape: - even odd - 3---2 3---2 - | / | | \ | - 0---1 0---1 - */ - - // Build all 4 positions in normalized cell space, using point-sampled height - Ogre::Vector3 v0 (startXTS, startYTS, getVertexHeight(land, startX, startY) / 8192.f); - Ogre::Vector3 v1 (endXTS, startYTS, getVertexHeight(land, endX, startY) / 8192.f); - Ogre::Vector3 v2 (endXTS, endYTS, getVertexHeight(land, endX, endY) / 8192.f); - Ogre::Vector3 v3 (startXTS, endYTS, getVertexHeight(land, startX, endY) / 8192.f); - // define this plane in terrain space - Ogre::Plane plane; - // (At the moment, all rows have the same triangle alignment) - if (true) - { - // odd row - bool secondTri = ((1.0 - yParam) > xParam); - if (secondTri) - plane.redefine(v0, v1, v3); - else - plane.redefine(v1, v2, v3); - } - else - { - // even row - bool secondTri = (yParam > xParam); - if (secondTri) - plane.redefine(v0, v2, v3); - else - plane.redefine(v0, v1, v2); - } - - // Solve plane equation for z - return (-plane.normal.x * nX - -plane.normal.y * nY - - plane.d) / plane.normal.z * 8192; - - } - - float TerrainStorage::getVertexHeight(const ESM::Land *land, int x, int y) - { - assert(x < ESM::Land::LAND_SIZE); - assert(y < ESM::Land::LAND_SIZE); - return land->mLandData->mHeights[y * ESM::Land::LAND_SIZE + x]; - } - - Terrain::LayerInfo TerrainStorage::getLayerInfo(const std::string& texture) - { - // Already have this cached? - if (mLayerInfoMap.find(texture) != mLayerInfoMap.end()) - return mLayerInfoMap[texture]; - - Terrain::LayerInfo info; - info.mParallax = false; - info.mSpecular = false; - info.mDiffuseMap = "textures\\" + texture; - std::string texture_ = texture; - boost::replace_last(texture_, ".", "_nh."); - if (Ogre::ResourceGroupManager::getSingleton().resourceExistsInAnyGroup("textures\\" + texture_)) - { - info.mNormalMap = "textures\\" + texture_; - info.mParallax = true; - } - else - { - texture_ = texture; - boost::replace_last(texture_, ".", "_n."); - if (Ogre::ResourceGroupManager::getSingleton().resourceExistsInAnyGroup("textures\\" + texture_)) - info.mNormalMap = "textures\\" + texture_; - } - - texture_ = texture; - boost::replace_last(texture_, ".", "_diffusespec."); - if (Ogre::ResourceGroupManager::getSingleton().resourceExistsInAnyGroup("textures\\" + texture_)) - { - info.mDiffuseMap = "textures\\" + texture_; - info.mSpecular = true; - } - - // This wasn't cached, so the textures are probably not loaded either. - // Background load them so they are hopefully already loaded once we need them! - Ogre::ResourceBackgroundQueue::getSingleton().load("Texture", info.mDiffuseMap, "General"); - if (!info.mNormalMap.empty()) - Ogre::ResourceBackgroundQueue::getSingleton().load("Texture", info.mNormalMap, "General"); - - mLayerInfoMap[texture] = info; - - return info; - } - - Terrain::LayerInfo TerrainStorage::getDefaultLayer() - { - Terrain::LayerInfo info; - info.mDiffuseMap = "textures\\_land_default.dds"; - info.mParallax = false; - info.mSpecular = false; - return info; - } - - float TerrainStorage::getCellWorldSize() - { - return ESM::Land::REAL_SIZE; - } - - int TerrainStorage::getCellVertices() - { - return ESM::Land::LAND_SIZE; - } - } diff --git a/apps/openmw/mwrender/terrainstorage.hpp b/apps/openmw/mwrender/terrainstorage.hpp index b5e6012f3..30a2a61ac 100644 --- a/apps/openmw/mwrender/terrainstorage.hpp +++ b/apps/openmw/mwrender/terrainstorage.hpp @@ -1,15 +1,13 @@ #ifndef MWRENDER_TERRAINSTORAGE_H #define MWRENDER_TERRAINSTORAGE_H -#include -#include - -#include +#include namespace MWRender { - class TerrainStorage : public Terrain::Storage + /// @brief Connects the ESM Store used in OpenMW with the ESMTerrain storage. + class TerrainStorage : public ESMTerrain::Storage { private: virtual ESM::Land* getLand (int cellX, int cellY); @@ -18,92 +16,6 @@ namespace MWRender /// Get bounds of the whole terrain in cell units virtual void getBounds(float& minX, float& maxX, float& minY, float& maxY); - - /// Get the minimum and maximum heights of a terrain region. - /// @note Will only be called for chunks with size = minBatchSize, i.e. leafs of the quad tree. - /// Larger chunks can simply merge AABB of children. - /// @param size size of the chunk in cell units - /// @param center center of the chunk in cell units - /// @param min min height will be stored here - /// @param max max height will be stored here - /// @return true if there was data available for this terrain chunk - virtual bool getMinMaxHeights (float size, const Ogre::Vector2& center, float& min, float& max); - - /// Fill vertex buffers for a terrain chunk. - /// @note May be called from background threads. Make sure to only call thread-safe functions from here! - /// @note returned colors need to be in render-system specific format! Use RenderSystem::convertColourValue. - /// @param lodLevel LOD level, 0 = most detailed - /// @param size size of the terrain chunk in cell units - /// @param center center of the chunk in cell units - /// @param positions buffer to write vertices - /// @param normals buffer to write vertex normals - /// @param colours buffer to write vertex colours - virtual void fillVertexBuffers (int lodLevel, float size, const Ogre::Vector2& center, Terrain::Alignment align, - std::vector& positions, - std::vector& normals, - std::vector& colours); - - /// Create textures holding layer blend values for a terrain chunk. - /// @note The terrain chunk shouldn't be larger than one cell since otherwise we might - /// have to do a ridiculous amount of different layers. For larger chunks, composite maps should be used. - /// @note May be called from *one* background thread. - /// @param chunkSize size of the terrain chunk in cell units - /// @param chunkCenter center of the chunk in cell units - /// @param pack Whether to pack blend values for up to 4 layers into one texture (one in each channel) - - /// otherwise, each texture contains blend values for one layer only. Shader-based rendering - /// can utilize packing, FFP can't. - /// @param blendmaps created blendmaps will be written here - /// @param layerList names of the layer textures used will be written here - virtual void getBlendmaps (float chunkSize, const Ogre::Vector2& chunkCenter, bool pack, - std::vector& blendmaps, - std::vector& layerList); - - /// Retrieve pixel data for textures holding layer blend values for terrain chunks and layer texture information. - /// This variant is provided to eliminate the overhead of virtual function calls when retrieving a large number of blendmaps at once. - /// @note The terrain chunks shouldn't be larger than one cell since otherwise we might - /// have to do a ridiculous amount of different layers. For larger chunks, composite maps should be used. - /// @note May be called from *one* background thread. - /// @param nodes A collection of nodes for which to retrieve the aforementioned data - /// @param out Output vector - /// @param pack Whether to pack blend values for up to 4 layers into one texture (one in each channel) - - /// otherwise, each texture contains blend values for one layer only. Shader-based rendering - /// can utilize packing, FFP can't. - virtual void getBlendmaps (const std::vector& nodes, std::vector& out, bool pack); - - virtual float getHeightAt (const Ogre::Vector3& worldPos); - - virtual Terrain::LayerInfo getDefaultLayer(); - - /// Get the transformation factor for mapping cell units to world units. - virtual float getCellWorldSize(); - - /// Get the number of vertices on one side for each cell. Should be (power of two)+1 - virtual int getCellVertices(); - - private: - void fixNormal (Ogre::Vector3& normal, int cellX, int cellY, int col, int row); - void fixColour (Ogre::ColourValue& colour, int cellX, int cellY, int col, int row); - void averageNormal (Ogre::Vector3& normal, int cellX, int cellY, int col, int row); - - float getVertexHeight (const ESM::Land* land, int x, int y); - - // Since plugins can define new texture palettes, we need to know the plugin index too - // in order to retrieve the correct texture name. - // pair - typedef std::pair UniqueTextureId; - - UniqueTextureId getVtexIndexAt(int cellX, int cellY, - int x, int y); - std::string getTextureName (UniqueTextureId id); - - std::map mLayerInfoMap; - - Terrain::LayerInfo getLayerInfo(const std::string& texture); - - // Non-virtual - void getBlendmapsImpl (float chunkSize, const Ogre::Vector2& chunkCenter, bool pack, - std::vector& blendmaps, - std::vector& layerList); }; } diff --git a/apps/openmw/mwrender/videoplayer.cpp b/apps/openmw/mwrender/videoplayer.cpp deleted file mode 100644 index 79f2fa948..000000000 --- a/apps/openmw/mwrender/videoplayer.cpp +++ /dev/null @@ -1,1181 +0,0 @@ -#include "videoplayer.hpp" - -#define __STDC_CONSTANT_MACROS -#include - -#include -#include - -#include -#include -#include - -#include - -#include "../mwbase/environment.hpp" -#include "../mwbase/soundmanager.hpp" -#include "../mwsound/sound_decoder.hpp" -#include "../mwsound/sound.hpp" - -#ifdef _WIN32 -#include - -typedef SSIZE_T ssize_t; -#endif - -namespace MWRender -{ - -#ifdef OPENMW_USE_FFMPEG - -extern "C" -{ - #include - #include - #include - - // From libavformat version 55.0.100 and onward the declaration of av_gettime() is removed from libavformat/avformat.h and moved - // to libavutil/time.h - // https://github.com/FFmpeg/FFmpeg/commit/06a83505992d5f49846c18507a6c3eb8a47c650e - #if AV_VERSION_INT(55, 0, 100) <= AV_VERSION_INT(LIBAVFORMAT_VERSION_MAJOR, LIBAVFORMAT_VERSION_MINOR, LIBAVFORMAT_VERSION_MICRO) - #include - #endif - - // From libavutil version 52.2.0 and onward the declaration of - // AV_CH_LAYOUT_* is removed from libavcodec/avcodec.h and moved to - // libavutil/channel_layout.h - #if AV_VERSION_INT(52, 2, 0) <= AV_VERSION_INT(LIBAVUTIL_VERSION_MAJOR, \ - LIBAVUTIL_VERSION_MINOR, LIBAVUTIL_VERSION_MICRO) - #include - #endif -} - -#ifdef _WIN32 - // Decide whether to play binkaudio. - #include - // libavcodec versions 54.10.100 (or maybe earlier) to 54.54.100 potentially crashes Windows 64bit. - // From version 54.56 or higher, there's no sound due to the encoding format changing from S16 to FLTP - // (see https://gitorious.org/ffmpeg/ffmpeg/commit/7bfd1766d1c18f07b0a2dd042418a874d49ea60d and - // http://git.videolan.org/?p=ffmpeg.git;a=commitdiff;h=3049d5b9b32845c86aa5588bb3352bdeb2edfdb2;hp=43c6b45a53a186a187f7266e4d6bd3c2620519f1), - // but does not crash (or at least no known crash). - #if (LIBAVCODEC_VERSION_MAJOR > 54) - #define FFMPEG_PLAY_BINKAUDIO - #else - #ifdef _WIN64 - #if ((LIBAVCODEC_VERSION_MAJOR == 54) && (LIBAVCODEC_VERSION_MINOR >= 55)) - #define FFMPEG_PLAY_BINKAUDIO - #endif - #else - #if ((LIBAVCODEC_VERSION_MAJOR == 54) && (LIBAVCODEC_VERSION_MINOR >= 10)) - #define FFMPEG_PLAY_BINKAUDIO - #endif - #endif - #endif -#endif - -#define MAX_AUDIOQ_SIZE (5 * 16 * 1024) -#define MAX_VIDEOQ_SIZE (5 * 256 * 1024) -#define AV_SYNC_THRESHOLD 0.01 -#define AUDIO_DIFF_AVG_NB 20 -#define VIDEO_PICTURE_QUEUE_SIZE 1 - -enum { - AV_SYNC_AUDIO_MASTER, - AV_SYNC_VIDEO_MASTER, - AV_SYNC_EXTERNAL_MASTER, - - AV_SYNC_DEFAULT = AV_SYNC_EXTERNAL_MASTER -}; - - -struct PacketQueue { - PacketQueue() - : first_pkt(NULL), last_pkt(NULL), flushing(false), nb_packets(0), size(0) - { } - ~PacketQueue() - { clear(); } - - AVPacketList *first_pkt, *last_pkt; - volatile bool flushing; - int nb_packets; - int size; - - boost::mutex mutex; - boost::condition_variable cond; - - void put(AVPacket *pkt); - int get(AVPacket *pkt, VideoState *is); - - void flush(); - void clear(); -}; - -struct VideoPicture { - VideoPicture() : pts(0.0) - { } - - std::vector data; - double pts; -}; - -struct VideoState { - VideoState() - : format_ctx(NULL), av_sync_type(AV_SYNC_DEFAULT) - , external_clock_base(0.0) - , audio_st(NULL) - , video_st(NULL), frame_last_pts(0.0), frame_last_delay(0.0), - video_clock(0.0), sws_context(NULL), rgbaFrame(NULL), pictq_size(0), - pictq_rindex(0), pictq_windex(0) - , refresh_rate_ms(10), refresh(false), quit(false), display_ready(false) - { - // Register all formats and codecs - av_register_all(); - } - - ~VideoState() - { deinit(); } - - void init(const std::string& resourceName); - void deinit(); - - int stream_open(int stream_index, AVFormatContext *pFormatCtx); - - bool update(); - - static void video_thread_loop(VideoState *is); - static void decode_thread_loop(VideoState *is); - - void video_display(); - void video_refresh_timer(); - - int queue_picture(AVFrame *pFrame, double pts); - double synchronize_video(AVFrame *src_frame, double pts); - - static void video_refresh(VideoState *is); - - - double get_audio_clock() - { return this->AudioTrack->getTimeOffset(); } - - double get_video_clock() - { return this->frame_last_pts; } - - double get_external_clock() - { return ((uint64_t)av_gettime()-this->external_clock_base) / 1000000.0; } - - double get_master_clock() - { - if(this->av_sync_type == AV_SYNC_VIDEO_MASTER) - return this->get_video_clock(); - if(this->av_sync_type == AV_SYNC_AUDIO_MASTER) - return this->get_audio_clock(); - return this->get_external_clock(); - } - - - static int OgreResource_Read(void *user_data, uint8_t *buf, int buf_size); - static int OgreResource_Write(void *user_data, uint8_t *buf, int buf_size); - static int64_t OgreResource_Seek(void *user_data, int64_t offset, int whence); - - Ogre::TexturePtr mTexture; - - Ogre::DataStreamPtr stream; - AVFormatContext* format_ctx; - - int av_sync_type; - uint64_t external_clock_base; - - AVStream** audio_st; - PacketQueue audioq; - MWBase::SoundPtr AudioTrack; - - AVStream** video_st; - double frame_last_pts; - double frame_last_delay; - double video_clock; ///pkt = *pkt; - pkt1->next = NULL; - - if(pkt1->pkt.destruct == NULL) - { - if(av_dup_packet(&pkt1->pkt) < 0) - { - av_free(pkt1); - throw std::runtime_error("Failed to duplicate packet"); - } - av_free_packet(pkt); - } - - this->mutex.lock (); - - if(!last_pkt) - this->first_pkt = pkt1; - else - this->last_pkt->next = pkt1; - this->last_pkt = pkt1; - this->nb_packets++; - this->size += pkt1->pkt.size; - this->cond.notify_one(); - - this->mutex.unlock(); -} - -int PacketQueue::get(AVPacket *pkt, VideoState *is) -{ - boost::unique_lock lock(this->mutex); - while(!is->quit) - { - AVPacketList *pkt1 = this->first_pkt; - if(pkt1) - { - this->first_pkt = pkt1->next; - if(!this->first_pkt) - this->last_pkt = NULL; - this->nb_packets--; - this->size -= pkt1->pkt.size; - - *pkt = pkt1->pkt; - av_free(pkt1); - - return 1; - } - - if(this->flushing) - break; - this->cond.wait(lock); - } - - return -1; -} - -void PacketQueue::flush() -{ - this->flushing = true; - this->cond.notify_one(); -} - -void PacketQueue::clear() -{ - AVPacketList *pkt, *pkt1; - - this->mutex.lock(); - for(pkt = this->first_pkt; pkt != NULL; pkt = pkt1) - { - pkt1 = pkt->next; - av_free_packet(&pkt->pkt); - av_freep(&pkt); - } - this->last_pkt = NULL; - this->first_pkt = NULL; - this->nb_packets = 0; - this->size = 0; - this->mutex.unlock (); -} - - -class MovieAudioDecoder : public MWSound::Sound_Decoder -{ - static void fail(const std::string &str) - { - throw std::runtime_error(str); - } - - struct AutoAVPacket : public AVPacket { - AutoAVPacket(int size=0) - { - if(av_new_packet(this, size) < 0) - throw std::bad_alloc(); - } - ~AutoAVPacket() - { av_free_packet(this); } - }; - - VideoState *mVideoState; - AVStream *mAVStream; - - AutoAVPacket mPacket; - AVFrame *mFrame; - ssize_t mFramePos; - ssize_t mFrameSize; - - double mAudioClock; - - /* averaging filter for audio sync */ - double mAudioDiffAccum; - const double mAudioDiffAvgCoef; - const double mAudioDiffThreshold; - int mAudioDiffAvgCount; - - /* Add or subtract samples to get a better sync, return number of bytes to - * skip (negative means to duplicate). */ - int synchronize_audio() - { - if(mVideoState->av_sync_type == AV_SYNC_AUDIO_MASTER) - return 0; - - int sample_skip = 0; - - // accumulate the clock difference - double diff = mVideoState->get_master_clock() - mVideoState->get_audio_clock(); - mAudioDiffAccum = diff + mAudioDiffAvgCoef * mAudioDiffAccum; - if(mAudioDiffAvgCount < AUDIO_DIFF_AVG_NB) - mAudioDiffAvgCount++; - else - { - double avg_diff = mAudioDiffAccum * (1.0 - mAudioDiffAvgCoef); - if(fabs(avg_diff) >= mAudioDiffThreshold) - { - int n = av_get_bytes_per_sample(mAVStream->codec->sample_fmt) * - mAVStream->codec->channels; - sample_skip = ((int)(diff * mAVStream->codec->sample_rate) * n); - } - } - - return sample_skip; - } - - int audio_decode_frame(AVFrame *frame) - { - AVPacket *pkt = &mPacket; - - for(;;) - { - while(pkt->size > 0) - { - int len1, got_frame; - - len1 = avcodec_decode_audio4(mAVStream->codec, frame, &got_frame, pkt); - if(len1 < 0) break; - - if(len1 <= pkt->size) - { - /* Move the unread data to the front and clear the end bits */ - int remaining = pkt->size - len1; - memmove(pkt->data, &pkt->data[len1], remaining); - av_shrink_packet(pkt, remaining); - } - - /* No data yet? Look for more frames */ - if(!got_frame || frame->nb_samples <= 0) - continue; - - mAudioClock += (double)frame->nb_samples / - (double)mAVStream->codec->sample_rate; - - /* We have data, return it and come back for more later */ - return frame->nb_samples * mAVStream->codec->channels * - av_get_bytes_per_sample(mAVStream->codec->sample_fmt); - } - av_free_packet(pkt); - - /* next packet */ - if(mVideoState->audioq.get(pkt, mVideoState) < 0) - return -1; - - /* if update, update the audio clock w/pts */ - if((uint64_t)pkt->pts != AV_NOPTS_VALUE) - mAudioClock = av_q2d(mAVStream->time_base)*pkt->pts; - } - } - - void open(const std::string&) -#ifdef _WIN32 - { fail(std::string("Invalid call to ")+__FUNCSIG__); } -#else - { fail(std::string("Invalid call to ")+__PRETTY_FUNCTION__); } -#endif - - void close() { } - - std::string getName() - { return mVideoState->stream->getName(); } - - void rewind() { } - -public: - MovieAudioDecoder(VideoState *is) - : mVideoState(is) - , mAVStream(*is->audio_st) - , mFrame(avcodec_alloc_frame()) - , mFramePos(0) - , mFrameSize(0) - , mAudioClock(0.0) - , mAudioDiffAccum(0.0) - , mAudioDiffAvgCoef(exp(log(0.01 / AUDIO_DIFF_AVG_NB))) - /* Correct audio only if larger error than this */ - , mAudioDiffThreshold(2.0 * 0.050/* 50 ms */) - , mAudioDiffAvgCount(0) - { } - virtual ~MovieAudioDecoder() - { - av_freep(&mFrame); - } - - void getInfo(int *samplerate, MWSound::ChannelConfig *chans, MWSound::SampleType * type) - { - if(mAVStream->codec->sample_fmt == AV_SAMPLE_FMT_U8) - *type = MWSound::SampleType_UInt8; - else if(mAVStream->codec->sample_fmt == AV_SAMPLE_FMT_S16) - *type = MWSound::SampleType_Int16; - else if(mAVStream->codec->sample_fmt == AV_SAMPLE_FMT_FLT) - *type = MWSound::SampleType_Float32; - else - fail(std::string("Unsupported sample format: ")+ - av_get_sample_fmt_name(mAVStream->codec->sample_fmt)); - - if(mAVStream->codec->channel_layout == AV_CH_LAYOUT_MONO) - *chans = MWSound::ChannelConfig_Mono; - else if(mAVStream->codec->channel_layout == AV_CH_LAYOUT_STEREO) - *chans = MWSound::ChannelConfig_Stereo; - else if(mAVStream->codec->channel_layout == AV_CH_LAYOUT_QUAD) - *chans = MWSound::ChannelConfig_Quad; - else if(mAVStream->codec->channel_layout == AV_CH_LAYOUT_5POINT1) - *chans = MWSound::ChannelConfig_5point1; - else if(mAVStream->codec->channel_layout == AV_CH_LAYOUT_7POINT1) - *chans = MWSound::ChannelConfig_7point1; - else if(mAVStream->codec->channel_layout == 0) - { - /* Unknown channel layout. Try to guess. */ - if(mAVStream->codec->channels == 1) - *chans = MWSound::ChannelConfig_Mono; - else if(mAVStream->codec->channels == 2) - *chans = MWSound::ChannelConfig_Stereo; - else - { - std::stringstream sstr("Unsupported raw channel count: "); - sstr << mAVStream->codec->channels; - fail(sstr.str()); - } - } - else - { - char str[1024]; - av_get_channel_layout_string(str, sizeof(str), mAVStream->codec->channels, - mAVStream->codec->channel_layout); - fail(std::string("Unsupported channel layout: ")+str); - } - - *samplerate = mAVStream->codec->sample_rate; - } - - size_t read(char *stream, size_t len) - { - int sample_skip = synchronize_audio(); - size_t total = 0; - - while(total < len) - { - if(mFramePos >= mFrameSize) - { - /* We have already sent all our data; get more */ - mFrameSize = audio_decode_frame(mFrame); - if(mFrameSize < 0) - { - /* If error, we're done */ - break; - } - - mFramePos = std::min(mFrameSize, sample_skip); - sample_skip -= mFramePos; - continue; - } - - size_t len1 = len - total; - if(mFramePos >= 0) - { - len1 = std::min(len1, mFrameSize-mFramePos); - memcpy(stream, mFrame->data[0]+mFramePos, len1); - } - else - { - len1 = std::min(len1, -mFramePos); - - int n = av_get_bytes_per_sample(mAVStream->codec->sample_fmt) * - mAVStream->codec->channels; - - /* add samples by copying the first sample*/ - if(n == 1) - memset(stream, *mFrame->data[0], len1); - else if(n == 2) - { - const int16_t val = *((int16_t*)mFrame->data[0]); - for(size_t nb = 0;nb < len1;nb += n) - *((int16_t*)(stream+nb)) = val; - } - else if(n == 4) - { - const int32_t val = *((int32_t*)mFrame->data[0]); - for(size_t nb = 0;nb < len1;nb += n) - *((int32_t*)(stream+nb)) = val; - } - else if(n == 8) - { - const int64_t val = *((int64_t*)mFrame->data[0]); - for(size_t nb = 0;nb < len1;nb += n) - *((int64_t*)(stream+nb)) = val; - } - else - { - for(size_t nb = 0;nb < len1;nb += n) - memcpy(stream+nb, mFrame->data[0], n); - } - } - - total += len1; - stream += len1; - mFramePos += len1; - } - - return total; - } - - size_t getSampleOffset() - { - ssize_t clock_delay = (mFrameSize-mFramePos) / mAVStream->codec->channels / - av_get_bytes_per_sample(mAVStream->codec->sample_fmt); - return (size_t)(mAudioClock*mAVStream->codec->sample_rate) - clock_delay; - } -}; - - -int VideoState::OgreResource_Read(void *user_data, uint8_t *buf, int buf_size) -{ - Ogre::DataStreamPtr stream = static_cast(user_data)->stream; - return stream->read(buf, buf_size); -} - -int VideoState::OgreResource_Write(void *user_data, uint8_t *buf, int buf_size) -{ - Ogre::DataStreamPtr stream = static_cast(user_data)->stream; - return stream->write(buf, buf_size); -} - -int64_t VideoState::OgreResource_Seek(void *user_data, int64_t offset, int whence) -{ - Ogre::DataStreamPtr stream = static_cast(user_data)->stream; - - whence &= ~AVSEEK_FORCE; - if(whence == AVSEEK_SIZE) - return stream->size(); - if(whence == SEEK_SET) - stream->seek(offset); - else if(whence == SEEK_CUR) - stream->seek(stream->tell()+offset); - else if(whence == SEEK_END) - stream->seek(stream->size()+offset); - else - return -1; - - return stream->tell(); -} - - -void VideoState::video_refresh(VideoState* is) -{ - boost::system_time t = boost::get_system_time(); - while(!is->quit) - { - t += boost::posix_time::milliseconds(is->refresh_rate_ms); - boost::this_thread::sleep(t); - is->refresh = true; - } -} - - -void VideoState::video_display() -{ - VideoPicture *vp = &this->pictq[this->pictq_rindex]; - - if((*this->video_st)->codec->width != 0 && (*this->video_st)->codec->height != 0) - { - - if(static_cast(mTexture->getWidth()) != (*this->video_st)->codec->width || - static_cast(mTexture->getHeight()) != (*this->video_st)->codec->height) - { - mTexture->unload(); - mTexture->setWidth((*this->video_st)->codec->width); - mTexture->setHeight((*this->video_st)->codec->height); - mTexture->createInternalResources(); - } - Ogre::PixelBox pb((*this->video_st)->codec->width, (*this->video_st)->codec->height, 1, Ogre::PF_BYTE_RGBA, &vp->data[0]); - Ogre::HardwarePixelBufferSharedPtr buffer = mTexture->getBuffer(); - buffer->blitFromMemory(pb); - this->display_ready = true; - } -} - -void VideoState::video_refresh_timer() -{ - VideoPicture *vp; - double delay; - - if(this->pictq_size == 0) - return; - - vp = &this->pictq[this->pictq_rindex]; - - delay = vp->pts - this->frame_last_pts; /* the pts from last time */ - if(delay <= 0 || delay >= 1.0) { - /* if incorrect delay, use previous one */ - delay = this->frame_last_delay; - } - /* save for next time */ - this->frame_last_delay = delay; - this->frame_last_pts = vp->pts; - - /* FIXME: Syncing should be done in the decoding stage, where frames can be - * skipped or duplicated as needed. */ - /* update delay to sync to audio if not master source */ - if(this->av_sync_type != AV_SYNC_VIDEO_MASTER) - { - double diff = this->get_video_clock() - this->get_master_clock(); - - /* Skip or repeat the frame. Take delay into account - * FFPlay still doesn't "know if this is the best guess." */ - double sync_threshold = std::max(delay, AV_SYNC_THRESHOLD); - if(diff <= -sync_threshold) - delay = 0; - else if(diff >= sync_threshold) - delay = 2 * delay; - } - - this->refresh_rate_ms = std::max(1, (int)(delay*1000.0)); - /* show the picture! */ - this->video_display(); - - /* update queue for next picture! */ - this->pictq_rindex = (this->pictq_rindex+1) % VIDEO_PICTURE_QUEUE_SIZE; - this->pictq_mutex.lock(); - this->pictq_size--; - this->pictq_cond.notify_one(); - this->pictq_mutex.unlock(); -} - - -int VideoState::queue_picture(AVFrame *pFrame, double pts) -{ - VideoPicture *vp; - - /* wait until we have a new pic */ - { - boost::unique_lock lock(this->pictq_mutex); - while(this->pictq_size >= VIDEO_PICTURE_QUEUE_SIZE && !this->quit) - this->pictq_cond.timed_wait(lock, boost::posix_time::milliseconds(1)); - } - if(this->quit) - return -1; - - // windex is set to 0 initially - vp = &this->pictq[this->pictq_windex]; - - // Convert the image into RGBA format for Ogre - if(this->sws_context == NULL) - { - int w = (*this->video_st)->codec->width; - int h = (*this->video_st)->codec->height; - this->sws_context = sws_getContext(w, h, (*this->video_st)->codec->pix_fmt, - w, h, PIX_FMT_RGBA, SWS_BICUBIC, - NULL, NULL, NULL); - if(this->sws_context == NULL) - throw std::runtime_error("Cannot initialize the conversion context!\n"); - } - - vp->pts = pts; - vp->data.resize((*this->video_st)->codec->width * (*this->video_st)->codec->height * 4); - - uint8_t *dst = &vp->data[0]; - sws_scale(this->sws_context, pFrame->data, pFrame->linesize, - 0, (*this->video_st)->codec->height, &dst, this->rgbaFrame->linesize); - - // now we inform our display thread that we have a pic ready - this->pictq_windex = (this->pictq_windex+1) % VIDEO_PICTURE_QUEUE_SIZE; - this->pictq_mutex.lock(); - this->pictq_size++; - this->pictq_mutex.unlock(); - - return 0; -} - -double VideoState::synchronize_video(AVFrame *src_frame, double pts) -{ - double frame_delay; - - /* if we have pts, set video clock to it */ - if(pts != 0) - this->video_clock = pts; - else - pts = this->video_clock; - - /* update the video clock */ - frame_delay = av_q2d((*this->video_st)->codec->time_base); - - /* if we are repeating a frame, adjust clock accordingly */ - frame_delay += src_frame->repeat_pict * (frame_delay * 0.5); - this->video_clock += frame_delay; - - return pts; -} - - -/* These are called whenever we allocate a frame - * buffer. We use this to store the global_pts in - * a frame at the time it is allocated. - */ -static uint64_t global_video_pkt_pts = AV_NOPTS_VALUE; -static int our_get_buffer(struct AVCodecContext *c, AVFrame *pic) -{ - int ret = avcodec_default_get_buffer(c, pic); - uint64_t *pts = (uint64_t*)av_malloc(sizeof(uint64_t)); - *pts = global_video_pkt_pts; - pic->opaque = pts; - return ret; -} -static void our_release_buffer(struct AVCodecContext *c, AVFrame *pic) -{ - if(pic) av_freep(&pic->opaque); - avcodec_default_release_buffer(c, pic); -} - - -void VideoState::video_thread_loop(VideoState *self) -{ - AVPacket pkt1, *packet = &pkt1; - int frameFinished; - AVFrame *pFrame; - double pts; - - pFrame = avcodec_alloc_frame(); - - self->rgbaFrame = avcodec_alloc_frame(); - avpicture_alloc((AVPicture*)self->rgbaFrame, PIX_FMT_RGBA, (*self->video_st)->codec->width, (*self->video_st)->codec->height); - - while(self->videoq.get(packet, self) >= 0) - { - // Save global pts to be stored in pFrame - global_video_pkt_pts = packet->pts; - // Decode video frame - if(avcodec_decode_video2((*self->video_st)->codec, pFrame, &frameFinished, packet) < 0) - throw std::runtime_error("Error decoding video frame"); - - pts = 0; - if((uint64_t)packet->dts != AV_NOPTS_VALUE) - pts = packet->dts; - else if(pFrame->opaque && *(uint64_t*)pFrame->opaque != AV_NOPTS_VALUE) - pts = *(uint64_t*)pFrame->opaque; - pts *= av_q2d((*self->video_st)->time_base); - - av_free_packet(packet); - - // Did we get a video frame? - if(frameFinished) - { - pts = self->synchronize_video(pFrame, pts); - if(self->queue_picture(pFrame, pts) < 0) - break; - } - } - - av_free(pFrame); - - avpicture_free((AVPicture*)self->rgbaFrame); - av_free(self->rgbaFrame); -} - -void VideoState::decode_thread_loop(VideoState *self) -{ - AVFormatContext *pFormatCtx = self->format_ctx; - AVPacket pkt1, *packet = &pkt1; - - try - { - if(!self->video_st && !self->audio_st) - throw std::runtime_error("No streams to decode"); - - // main decode loop - while(!self->quit) - { - if((self->audio_st && self->audioq.size > MAX_AUDIOQ_SIZE) || - (self->video_st && self->videoq.size > MAX_VIDEOQ_SIZE)) - { - boost::this_thread::sleep(boost::posix_time::milliseconds(10)); - continue; - } - - if(av_read_frame(pFormatCtx, packet) < 0) - break; - - // Is this a packet from the video stream? - if(self->video_st && packet->stream_index == self->video_st-pFormatCtx->streams) - self->videoq.put(packet); - else if(self->audio_st && packet->stream_index == self->audio_st-pFormatCtx->streams) - self->audioq.put(packet); - else - av_free_packet(packet); - } - - /* all done - wait for it */ - self->videoq.flush(); - self->audioq.flush(); - while(!self->quit) - { - // EOF reached, all packets processed, we can exit now - if(self->audioq.nb_packets == 0 && self->videoq.nb_packets == 0) - break; - boost::this_thread::sleep(boost::posix_time::milliseconds(100)); - } - } - catch(std::runtime_error& e) { - std::cerr << "An error occured playing the video: " << e.what () << std::endl; - } - catch(Ogre::Exception& e) { - std::cerr << "An error occured playing the video: " << e.getFullDescription () << std::endl; - } - - self->quit = true; -} - - -bool VideoState::update() -{ - if(this->quit) - return false; - - if(this->refresh) - { - this->refresh = false; - this->video_refresh_timer(); - } - return true; -} - - -int VideoState::stream_open(int stream_index, AVFormatContext *pFormatCtx) -{ - MWSound::DecoderPtr decoder; - AVCodecContext *codecCtx; - AVCodec *codec; - - if(stream_index < 0 || stream_index >= static_cast(pFormatCtx->nb_streams)) - return -1; - - // Get a pointer to the codec context for the video stream - codecCtx = pFormatCtx->streams[stream_index]->codec; - codec = avcodec_find_decoder(codecCtx->codec_id); - if(!codec || (avcodec_open2(codecCtx, codec, NULL) < 0)) - { - fprintf(stderr, "Unsupported codec!\n"); - return -1; - } - - switch(codecCtx->codec_type) - { - case AVMEDIA_TYPE_AUDIO: - this->audio_st = pFormatCtx->streams + stream_index; - - decoder.reset(new MovieAudioDecoder(this)); - this->AudioTrack = MWBase::Environment::get().getSoundManager()->playTrack(decoder, MWBase::SoundManager::Play_TypeMovie); - if(!this->AudioTrack) - { - avcodec_close((*this->audio_st)->codec); - this->audio_st = NULL; - return -1; - } - break; - - case AVMEDIA_TYPE_VIDEO: - this->video_st = pFormatCtx->streams + stream_index; - - this->frame_last_delay = 40e-3; - - codecCtx->get_buffer = our_get_buffer; - codecCtx->release_buffer = our_release_buffer; - this->video_thread = boost::thread(video_thread_loop, this); - this->refresh_thread = boost::thread(video_refresh, this); - break; - - default: - break; - } - - return 0; -} - -void VideoState::init(const std::string& resourceName) -{ - int video_index = -1; - int audio_index = -1; - unsigned int i; - - this->av_sync_type = AV_SYNC_DEFAULT; - this->refresh_rate_ms = 10; - this->refresh = false; - this->quit = false; - - this->stream = Ogre::ResourceGroupManager::getSingleton().openResource(resourceName); - if(this->stream.isNull()) - throw std::runtime_error("Failed to open video resource"); - - AVIOContext *ioCtx = avio_alloc_context(NULL, 0, 0, this, OgreResource_Read, OgreResource_Write, OgreResource_Seek); - if(!ioCtx) throw std::runtime_error("Failed to allocate AVIOContext"); - - this->format_ctx = avformat_alloc_context(); - if(this->format_ctx) - this->format_ctx->pb = ioCtx; - - // Open video file - /// - /// format_ctx->pb->buffer must be freed by hand, - /// if not, valgrind will show memleak, see: - /// - /// https://trac.ffmpeg.org/ticket/1357 - /// - if(!this->format_ctx || avformat_open_input(&this->format_ctx, resourceName.c_str(), NULL, NULL)) - { - if (this->format_ctx != NULL) - { - if (this->format_ctx->pb != NULL) - { - av_free(this->format_ctx->pb->buffer); - this->format_ctx->pb->buffer = NULL; - - av_free(this->format_ctx->pb); - this->format_ctx->pb = NULL; - } - } - // "Note that a user-supplied AVFormatContext will be freed on failure." - this->format_ctx = NULL; - av_free(ioCtx); - throw std::runtime_error("Failed to open video input"); - } - - // Retrieve stream information - if(avformat_find_stream_info(this->format_ctx, NULL) < 0) - throw std::runtime_error("Failed to retrieve stream information"); - - // Dump information about file onto standard error - av_dump_format(this->format_ctx, 0, resourceName.c_str(), 0); - - for(i = 0;i < this->format_ctx->nb_streams;i++) - { - if(this->format_ctx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO && video_index < 0) - video_index = i; - if(this->format_ctx->streams[i]->codec->codec_type == AVMEDIA_TYPE_AUDIO && audio_index < 0) - audio_index = i; - } - - if (audio_index != -1) - MWBase::Environment::get().getSoundManager()->pauseSounds(); - - this->external_clock_base = av_gettime(); - -#if !defined(_WIN32) || defined(FFMPEG_PLAY_BINKAUDIO) - if(audio_index >= 0) - this->stream_open(audio_index, this->format_ctx); -#else - std::cout<<"FFmpeg sound disabled for \""+resourceName+"\""<= 0) - { - this->stream_open(video_index, this->format_ctx); - - int width = (*this->video_st)->codec->width; - int height = (*this->video_st)->codec->height; - static int i = 0; - this->mTexture = Ogre::TextureManager::getSingleton().createManual( - "OpenMW/VideoTexture" + Ogre::StringConverter::toString(++i), - Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME, - Ogre::TEX_TYPE_2D, - width, height, // TEST - 0, - Ogre::PF_BYTE_RGBA, - Ogre::TU_DYNAMIC_WRITE_ONLY_DISCARDABLE); - - // initialize to (0,0,0,0) - std::vector buffer; - buffer.resize(width * height, 0); - Ogre::PixelBox pb(width, height, 1, Ogre::PF_BYTE_RGBA, &buffer[0]); - this->mTexture->getBuffer()->blitFromMemory(pb); - } - - - this->parse_thread = boost::thread(decode_thread_loop, this); -} - -void VideoState::deinit() -{ - this->quit = true; - - this->audioq.cond.notify_one(); - this->videoq.cond.notify_one(); - - if (this->parse_thread.joinable()) - this->parse_thread.join(); - if (this->video_thread.joinable()) - this->video_thread.join(); - if (this->refresh_thread.joinable()) - this->refresh_thread.join(); - - if(this->audio_st) - avcodec_close((*this->audio_st)->codec); - this->audio_st = NULL; - if(this->video_st) - avcodec_close((*this->video_st)->codec); - this->video_st = NULL; - - if(this->sws_context) - sws_freeContext(this->sws_context); - this->sws_context = NULL; - - if(this->format_ctx) - { - /// - /// format_ctx->pb->buffer must be freed by hand, - /// if not, valgrind will show memleak, see: - /// - /// https://trac.ffmpeg.org/ticket/1357 - /// - if (this->format_ctx->pb != NULL) - { - av_free(this->format_ctx->pb->buffer); - this->format_ctx->pb->buffer = NULL; - - av_free(this->format_ctx->pb); - this->format_ctx->pb = NULL;; - } - avformat_close_input(&this->format_ctx); - } -} - -#else // defined OPENMW_USE_FFMPEG - -class VideoState -{ -public: - VideoState() { } - - void init(const std::string& resourceName) - { - throw std::runtime_error("FFmpeg not supported, cannot play \""+resourceName+"\""); - } - void deinit() { } - - void close() { } - - bool update(Ogre::MaterialPtr &mat, Ogre::Rectangle2D *rect, int screen_width, int screen_height) - { return false; } -}; - -#endif // defined OPENMW_USE_FFMPEG - - -VideoPlayer::VideoPlayer() - : mState(NULL) -{ - -} - -VideoPlayer::~VideoPlayer() -{ - if(mState) - close(); -} - -void VideoPlayer::playVideo(const std::string &resourceName) -{ - if(mState) - close(); - - try { - mState = new VideoState; - mState->init(resourceName); - } - catch(std::exception& e) { - std::cerr<< "Failed to play video: "<update()) - close(); - } -} - -std::string VideoPlayer::getTextureName() -{ - std::string name; - if (mState) - name = mState->mTexture->getName(); - return name; -} - -int VideoPlayer::getVideoWidth() -{ - int width=0; - if (mState) - width = mState->mTexture->getWidth(); - return width; -} - -int VideoPlayer::getVideoHeight() -{ - int height=0; - if (mState) - height = mState->mTexture->getHeight(); - return height; -} - -void VideoPlayer::stopVideo () -{ - close(); -} - -void VideoPlayer::close() -{ - if(mState) - { - mState->deinit(); - - delete mState; - mState = NULL; - } - - MWBase::Environment::get().getSoundManager()->resumeSounds(); -} - -bool VideoPlayer::isPlaying () -{ - return mState != NULL; -} - -} diff --git a/apps/openmw/mwrender/videoplayer.hpp b/apps/openmw/mwrender/videoplayer.hpp deleted file mode 100644 index 47e252cc1..000000000 --- a/apps/openmw/mwrender/videoplayer.hpp +++ /dev/null @@ -1,41 +0,0 @@ -#ifndef VIDEOPLAYER_H -#define VIDEOPLAYER_H - -#include - -namespace MWRender -{ - struct VideoState; - - /** - * @brief Plays a video on an Ogre texture. - */ - class VideoPlayer - { - public: - VideoPlayer(); - ~VideoPlayer(); - - void playVideo (const std::string& resourceName); - - void update(); - - void close(); - void stopVideo(); - - bool isPlaying(); - - std::string getTextureName(); - int getVideoWidth(); - int getVideoHeight(); - - - private: - VideoState* mState; - - int mWidth; - int mHeight; - }; -} - -#endif diff --git a/apps/openmw/mwrender/water.cpp b/apps/openmw/mwrender/water.cpp index 2cbc4462c..fd790b363 100644 --- a/apps/openmw/mwrender/water.cpp +++ b/apps/openmw/mwrender/water.cpp @@ -74,7 +74,8 @@ CubeReflection::~CubeReflection () void CubeReflection::update () { - mParentCamera->getParentSceneNode ()->needUpdate (); + if (mParentCamera->isAttached()) + mParentCamera->getParentSceneNode ()->needUpdate (); mCamera->setPosition(mParentCamera->getDerivedPosition()); } @@ -133,7 +134,8 @@ void PlaneReflection::renderQueueEnded (Ogre::uint8 queueGroupId, const Ogre::St void PlaneReflection::preRenderTargetUpdate(const Ogre::RenderTargetEvent& evt) { - mParentCamera->getParentSceneNode ()->needUpdate (); + if (mParentCamera->isAttached()) + mParentCamera->getParentSceneNode ()->needUpdate (); mCamera->setOrientation(mParentCamera->getDerivedOrientation()); mCamera->setPosition(mParentCamera->getDerivedPosition()); mCamera->setNearClipDistance(mParentCamera->getNearClipDistance()); @@ -384,10 +386,11 @@ void Water::update(float dt, Ogre::Vector3 player) void Water::frameStarted(float dt) { - mSimulation->update(dt, mPlayer); - if (mReflection) + { + mSimulation->update(dt, mPlayer); mReflection->update(); + } } void Water::applyRTT() diff --git a/apps/openmw/mwrender/weaponanimation.cpp b/apps/openmw/mwrender/weaponanimation.cpp index 5f953bd83..a409e8807 100644 --- a/apps/openmw/mwrender/weaponanimation.cpp +++ b/apps/openmw/mwrender/weaponanimation.cpp @@ -81,7 +81,7 @@ void WeaponAnimation::releaseArrow(MWWorld::Ptr actor) const float fWeaponFatigueMult = gmst.find("fWeaponFatigueMult")->getFloat(); MWMechanics::CreatureStats& attackerStats = actor.getClass().getCreatureStats(actor); MWMechanics::DynamicStat fatigue = attackerStats.getFatigue(); - const float normalizedEncumbrance = actor.getClass().getEncumbrance(actor) / actor.getClass().getCapacity(actor); + const float normalizedEncumbrance = actor.getClass().getNormalizedEncumbrance(actor); float fatigueLoss = fFatigueAttackBase + normalizedEncumbrance * fFatigueAttackMult; if (!weapon->isEmpty()) fatigueLoss += weapon->getClass().getWeight(*weapon) * attackerStats.getAttackStrength() * fWeaponFatigueMult; @@ -150,9 +150,19 @@ void WeaponAnimation::pitchSkeleton(float xrot, Ogre::SkeletonInstance* skel) return; float pitch = xrot * mPitchFactor; - Ogre::Node *node = skel->getBone("Bip01 Spine2"); + Ogre::Node *node; + + // In spherearcher.nif, we have spine, not Spine. Not sure if all bone names should be case insensitive? + if (skel->hasBone("Bip01 spine2")) + node = skel->getBone("Bip01 spine2"); + else + node = skel->getBone("Bip01 Spine2"); node->pitch(Ogre::Radian(-pitch/2), Ogre::Node::TS_WORLD); - node = skel->getBone("Bip01 Spine1"); + + if (skel->hasBone("Bip01 spine1")) // in spherearcher.nif + node = skel->getBone("Bip01 spine1"); + else + node = skel->getBone("Bip01 Spine1"); node->pitch(Ogre::Radian(-pitch/2), Ogre::Node::TS_WORLD); } diff --git a/apps/openmw/mwscript/aiextensions.cpp b/apps/openmw/mwscript/aiextensions.cpp index cc17905df..a3f935487 100644 --- a/apps/openmw/mwscript/aiextensions.cpp +++ b/apps/openmw/mwscript/aiextensions.cpp @@ -186,7 +186,7 @@ namespace MWScript Interpreter::Type_Integer time = runtime[0].mFloat; runtime.pop(); - std::vector idleList; + std::vector idleList; bool repeat = false; for(int i=1; i < 10 && arg0; ++i) @@ -194,6 +194,7 @@ namespace MWScript if(!repeat) repeat = true; Interpreter::Type_Integer idleValue = runtime[0].mInteger; + idleValue = std::min(255, std::max(0, idleValue)); idleList.push_back(idleValue); runtime.pop(); --arg0; @@ -370,6 +371,12 @@ namespace MWScript MWWorld::Ptr actor = MWBase::Environment::get().getWorld()->getPtr(actorID, true); + if(!actor.getClass().isActor() || !observer.getClass().isActor()) + { + runtime.push(0); + return; + } + Interpreter::Type_Integer value = MWBase::Environment::get().getWorld()->getLOS(observer, actor) && MWBase::Environment::get().getMechanicsManager()->awarenessCheck(actor, observer); @@ -391,9 +398,10 @@ namespace MWScript std::string actorID = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); + MWWorld::Ptr dest = MWBase::Environment::get().getWorld()->getPtr(actorID,true); bool value = false; - if(dest != MWWorld::Ptr() ) + if(dest != MWWorld::Ptr() && source.getClass().isActor() && dest.getClass().isActor()) { value = MWBase::Environment::get().getWorld()->getLOS(source,dest); } @@ -412,7 +420,6 @@ namespace MWScript runtime.pop(); const MWMechanics::CreatureStats& creatureStats = actor.getClass().getCreatureStats(actor); - std::string currentTargetId; bool targetsAreEqual = false; MWWorld::Ptr targetPtr; @@ -449,7 +456,6 @@ namespace MWScript MWWorld::Ptr actor = R()(runtime); MWMechanics::CreatureStats& creatureStats = actor.getClass().getCreatureStats(actor); creatureStats.getAiSequence().stopCombat(); - creatureStats.setHostile(false); } }; @@ -469,6 +475,16 @@ namespace MWScript } }; + template + class OpFace : public Interpreter::Opcode0 + { + public: + virtual void execute(Interpreter::Runtime& runtime) + { + /// \todo implement + } + }; + void installOpcodes (Interpreter::Interpreter& interpreter) { interpreter.installSegment3 (Compiler::Ai::opcodeAIActivate, new OpAiActivate); @@ -530,6 +546,9 @@ namespace MWScript interpreter.installSegment5 (Compiler::Ai::opcodeGetFleeExplicit, new OpGetAiSetting(2)); interpreter.installSegment5 (Compiler::Ai::opcodeGetAlarm, new OpGetAiSetting(3)); interpreter.installSegment5 (Compiler::Ai::opcodeGetAlarmExplicit, new OpGetAiSetting(3)); + + interpreter.installSegment5 (Compiler::Ai::opcodeFace, new OpFace); + interpreter.installSegment5 (Compiler::Ai::opcodeFaceExplicit, new OpFace); } } } diff --git a/apps/openmw/mwscript/cellextensions.cpp b/apps/openmw/mwscript/cellextensions.cpp index 2e2e9b698..a568b7943 100644 --- a/apps/openmw/mwscript/cellextensions.cpp +++ b/apps/openmw/mwscript/cellextensions.cpp @@ -47,6 +47,7 @@ namespace MWScript if (world->findExteriorPosition(cell, pos)) { world->changeToExteriorCell(pos); + world->fixPosition(world->getPlayerPtr()); } else { @@ -79,6 +80,7 @@ namespace MWScript pos.rot[0] = pos.rot[1] = pos.rot[2] = 0; world->changeToExteriorCell (pos); + world->fixPosition(world->getPlayerPtr()); } }; @@ -151,7 +153,7 @@ namespace MWScript if (cell->getCell()->hasWater()) runtime.push (cell->getWaterLevel()); else - runtime.push (-std::numeric_limits().max()); + runtime.push (-std::numeric_limits::max()); } }; diff --git a/apps/openmw/mwscript/containerextensions.cpp b/apps/openmw/mwscript/containerextensions.cpp index 93711d036..96e30e396 100644 --- a/apps/openmw/mwscript/containerextensions.cpp +++ b/apps/openmw/mwscript/containerextensions.cpp @@ -290,18 +290,15 @@ namespace MWScript const std::string &name = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); + int count = 0; MWWorld::InventoryStore& invStore = ptr.getClass().getInventoryStore (ptr); for (MWWorld::ContainerStoreIterator it = invStore.begin(MWWorld::ContainerStore::Type_Miscellaneous); it != invStore.end(); ++it) { - if (::Misc::StringUtils::ciEqual(it->getCellRef().getSoul(), name)) - { - runtime.push(1); - return; - } + ++count; } - runtime.push(0); + runtime.push(count); } }; diff --git a/apps/openmw/mwscript/controlextensions.cpp b/apps/openmw/mwscript/controlextensions.cpp index d2e774859..fb6b73be6 100644 --- a/apps/openmw/mwscript/controlextensions.cpp +++ b/apps/openmw/mwscript/controlextensions.cpp @@ -121,6 +121,34 @@ namespace MWScript } }; + template + class OpGetForceJump : public Interpreter::Opcode0 + { + public: + + virtual void execute (Interpreter::Runtime& runtime) + { + MWWorld::Ptr ptr = R()(runtime); + + MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats (ptr); + runtime.push (stats.getMovementFlag (MWMechanics::CreatureStats::Flag_ForceJump)); + } + }; + + template + class OpGetForceMoveJump : public Interpreter::Opcode0 + { + public: + + virtual void execute (Interpreter::Runtime& runtime) + { + MWWorld::Ptr ptr = R()(runtime); + + MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats (ptr); + runtime.push (stats.getMovementFlag (MWMechanics::CreatureStats::Flag_ForceMoveJump)); + } + }; + template class OpGetForceSneak : public Interpreter::Opcode0 { @@ -169,27 +197,54 @@ namespace MWScript interpreter.installSegment5 (Compiler::Control::opcodeToggleCollision, new OpToggleCollision); + //Force Run interpreter.installSegment5 (Compiler::Control::opcodeClearForceRun, new OpClearMovementFlag (MWMechanics::CreatureStats::Flag_ForceRun)); - interpreter.installSegment5 (Compiler::Control::opcodeForceRun, - new OpSetMovementFlag (MWMechanics::CreatureStats::Flag_ForceRun)); - interpreter.installSegment5 (Compiler::Control::opcodeClearForceSneak, - new OpClearMovementFlag (MWMechanics::CreatureStats::Flag_ForceSneak)); - interpreter.installSegment5 (Compiler::Control::opcodeForceSneak, - new OpSetMovementFlag (MWMechanics::CreatureStats::Flag_ForceSneak)); - interpreter.installSegment5 (Compiler::Control::opcodeClearForceRunExplicit, new OpClearMovementFlag (MWMechanics::CreatureStats::Flag_ForceRun)); + interpreter.installSegment5 (Compiler::Control::opcodeForceRun, + new OpSetMovementFlag (MWMechanics::CreatureStats::Flag_ForceRun)); interpreter.installSegment5 (Compiler::Control::opcodeForceRunExplicit, new OpSetMovementFlag (MWMechanics::CreatureStats::Flag_ForceRun)); + + //Force Jump + interpreter.installSegment5 (Compiler::Control::opcodeClearForceJump, + new OpClearMovementFlag (MWMechanics::CreatureStats::Flag_ForceJump)); + interpreter.installSegment5 (Compiler::Control::opcodeClearForceJumpExplicit, + new OpClearMovementFlag (MWMechanics::CreatureStats::Flag_ForceJump)); + interpreter.installSegment5 (Compiler::Control::opcodeForceJump, + new OpSetMovementFlag (MWMechanics::CreatureStats::Flag_ForceJump)); + interpreter.installSegment5 (Compiler::Control::opcodeForceJumpExplicit, + new OpSetMovementFlag (MWMechanics::CreatureStats::Flag_ForceJump)); + + //Force MoveJump + interpreter.installSegment5 (Compiler::Control::opcodeClearForceMoveJump, + new OpClearMovementFlag (MWMechanics::CreatureStats::Flag_ForceMoveJump)); + interpreter.installSegment5 (Compiler::Control::opcodeClearForceMoveJumpExplicit, + new OpClearMovementFlag (MWMechanics::CreatureStats::Flag_ForceMoveJump)); + interpreter.installSegment5 (Compiler::Control::opcodeForceMoveJump, + new OpSetMovementFlag (MWMechanics::CreatureStats::Flag_ForceMoveJump)); + interpreter.installSegment5 (Compiler::Control::opcodeForceMoveJumpExplicit, + new OpSetMovementFlag (MWMechanics::CreatureStats::Flag_ForceMoveJump)); + + //Force Sneak + interpreter.installSegment5 (Compiler::Control::opcodeClearForceSneak, + new OpClearMovementFlag (MWMechanics::CreatureStats::Flag_ForceSneak)); interpreter.installSegment5 (Compiler::Control::opcodeClearForceSneakExplicit, new OpClearMovementFlag (MWMechanics::CreatureStats::Flag_ForceSneak)); + interpreter.installSegment5 (Compiler::Control::opcodeForceSneak, + new OpSetMovementFlag (MWMechanics::CreatureStats::Flag_ForceSneak)); interpreter.installSegment5 (Compiler::Control::opcodeForceSneakExplicit, new OpSetMovementFlag (MWMechanics::CreatureStats::Flag_ForceSneak)); + interpreter.installSegment5 (Compiler::Control::opcodeGetPcRunning, new OpGetPcRunning); interpreter.installSegment5 (Compiler::Control::opcodeGetPcSneaking, new OpGetPcSneaking); interpreter.installSegment5 (Compiler::Control::opcodeGetForceRun, new OpGetForceRun); interpreter.installSegment5 (Compiler::Control::opcodeGetForceRunExplicit, new OpGetForceRun); + interpreter.installSegment5 (Compiler::Control::opcodeGetForceJump, new OpGetForceJump); + interpreter.installSegment5 (Compiler::Control::opcodeGetForceJumpExplicit, new OpGetForceJump); + interpreter.installSegment5 (Compiler::Control::opcodeGetForceMoveJump, new OpGetForceMoveJump); + interpreter.installSegment5 (Compiler::Control::opcodeGetForceMoveJumpExplicit, new OpGetForceMoveJump); interpreter.installSegment5 (Compiler::Control::opcodeGetForceSneak, new OpGetForceSneak); interpreter.installSegment5 (Compiler::Control::opcodeGetForceSneakExplicit, new OpGetForceSneak); } diff --git a/apps/openmw/mwscript/dialogueextensions.cpp b/apps/openmw/mwscript/dialogueextensions.cpp index 9dde65ab2..563a9dde3 100644 --- a/apps/openmw/mwscript/dialogueextensions.cpp +++ b/apps/openmw/mwscript/dialogueextensions.cpp @@ -41,7 +41,8 @@ namespace MWScript } catch (...) { - MWBase::Environment::get().getJournal()->setJournalIndex(quest, index); + if (MWBase::Environment::get().getJournal()->getJournalIndex(quest) < index) + MWBase::Environment::get().getJournal()->setJournalIndex(quest, index); } } }; @@ -124,6 +125,9 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); + if (!ptr.getRefData().isEnabled()) + return; + MWBase::Environment::get().getDialogueManager()->startDialogue (ptr); } }; @@ -227,14 +231,23 @@ namespace MWScript std::string faction2 = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); - // ignore extra garbage argument - runtime.pop(); - runtime.push(MWBase::Environment::get().getDialogueManager() ->getFactionReaction(faction1, faction2)); } }; + template + class OpClearInfoActor : public Interpreter::Opcode0 + { + public: + virtual void execute (Interpreter::Runtime& runtime) + { + MWWorld::Ptr ptr = R()(runtime); + + MWBase::Environment::get().getDialogueManager()->clearInfoActor(ptr); + } + }; + void installOpcodes (Interpreter::Interpreter& interpreter) { @@ -256,6 +269,8 @@ namespace MWScript interpreter.installSegment5 (Compiler::Dialogue::opcodeSameFactionExplicit, new OpSameFaction); interpreter.installSegment5 (Compiler::Dialogue::opcodeModFactionReaction, new OpModFactionReaction); interpreter.installSegment5 (Compiler::Dialogue::opcodeGetFactionReaction, new OpGetFactionReaction); + interpreter.installSegment5 (Compiler::Dialogue::opcodeClearInfoActor, new OpClearInfoActor); + interpreter.installSegment5 (Compiler::Dialogue::opcodeClearInfoActorExplicit, new OpClearInfoActor); } } diff --git a/apps/openmw/mwscript/docs/vmformat.txt b/apps/openmw/mwscript/docs/vmformat.txt index 24b0b6f7a..b80c84d67 100644 --- a/apps/openmw/mwscript/docs/vmformat.txt +++ b/apps/openmw/mwscript/docs/vmformat.txt @@ -57,7 +57,8 @@ op 0x20028: RemoveSoulGem, explicit reference op 0x20029: PCRaiseRank, explicit reference op 0x2002a: PCLowerRank, explicit reference op 0x2002b: PCJoinFaction, explicit reference -opcodes 0x2002c-0x3ffff unused +op 0x2002c: MenuTest +opcodes 0x2002d-0x3ffff unused Segment 4: (not implemented yet) @@ -393,5 +394,44 @@ op 0x2000241: onKnockoutExplicit op 0x2000242: ModFactionReaction op 0x2000243: GetFactionReaction op 0x2000244: Activate, explicit +op 0x2000245: ClearInfoActor +op 0x2000246: ClearInfoActor, explicit +op 0x2000247: BetaComment +op 0x2000248: BetaComment, explicit +op 0x2000249: OnMurder +op 0x200024a: OnMurder, explicit +op 0x200024b: ToggleMenus +op 0x200024c: Face +op 0x200024d: Face, explicit +op 0x200024e: GetStat (dummy function) +op 0x200024f: GetStat (dummy function), explicit +op 0x2000250: GetCollidingPC +op 0x2000251: GetCollidingPC, explicit +op 0x2000252: GetCollidingActor +op 0x2000253: GetCollidingActor, explicit +op 0x2000254: HurtStandingActor +op 0x2000255: HurtStandingActor, explicit +op 0x2000256: HurtCollidingActor +op 0x2000257: HurtCollidingActor, explicit +op 0x2000258: ClearForceJump +op 0x2000259: ClearForceJump, explicit reference +op 0x200025a: ForceJump +op 0x200025b: ForceJump, explicit reference +op 0x200025c: ClearForceMoveJump +op 0x200025d: ClearForceMoveJump, explicit reference +op 0x200025e: ForceMoveJump +op 0x200025f: ForceMoveJump, explicit reference +op 0x2000260: GetForceJump +op 0x2000261: GetForceJump, explicit reference +op 0x2000262: GetForceMoveJump +op 0x2000263: GetForceMoveJump, explicit reference +op 0x2000264-0x200027b: GetMagicEffect +op 0x200027c-0x2000293: GetMagicEffect, explicit +op 0x2000294-0x20002ab: SetMagicEffect +op 0x20002ac-0x20002c3: SetMagicEffect, explicit +op 0x20002c4-0x20002db: ModMagicEffect +op 0x20002dc-0x20002f3: ModMagicEffect, explicit +op 0x20002f4: ResetActors +op 0x20002f5: ToggleWorld -opcodes 0x2000245-0x3ffffff unused +opcodes 0x20002f6-0x3ffffff unused diff --git a/apps/openmw/mwscript/globalscripts.cpp b/apps/openmw/mwscript/globalscripts.cpp index 8e118e2f8..b409a304c 100644 --- a/apps/openmw/mwscript/globalscripts.cpp +++ b/apps/openmw/mwscript/globalscripts.cpp @@ -15,61 +15,69 @@ namespace MWScript { + GlobalScriptDesc::GlobalScriptDesc() : mRunning (false) {} + + GlobalScripts::GlobalScripts (const MWWorld::ESMStore& store) : mStore (store) - { - addStartup(); - } + {} - void GlobalScripts::addScript (const std::string& name) + void GlobalScripts::addScript (const std::string& name, const std::string& targetId) { - std::map >::iterator iter = + std::map::iterator iter = mScripts.find (::Misc::StringUtils::lowerCase (name)); if (iter==mScripts.end()) { if (const ESM::Script *script = mStore.get().find (name)) { - Locals locals; - - locals.configure (*script); + GlobalScriptDesc desc; + desc.mRunning = true; + desc.mLocals.configure (*script); + desc.mId = targetId; - mScripts.insert (std::make_pair (name, std::make_pair (true, locals))); + mScripts.insert (std::make_pair (name, desc)); } } - else - iter->second.first = true; + else if (!iter->second.mRunning) + { + iter->second.mRunning = true; + iter->second.mId = targetId; + } } void GlobalScripts::removeScript (const std::string& name) { - std::map >::iterator iter = + std::map::iterator iter = mScripts.find (::Misc::StringUtils::lowerCase (name)); if (iter!=mScripts.end()) - iter->second.first = false; + iter->second.mRunning = false; } bool GlobalScripts::isRunning (const std::string& name) const { - std::map >::const_iterator iter = + std::map::const_iterator iter = mScripts.find (::Misc::StringUtils::lowerCase (name)); if (iter==mScripts.end()) return false; - return iter->second.first; + return iter->second.mRunning; } void GlobalScripts::run() { - for (std::map >::iterator iter (mScripts.begin()); + for (std::map::iterator iter (mScripts.begin()); iter!=mScripts.end(); ++iter) { - if (iter->second.first) + if (iter->second.mRunning) { + MWWorld::Ptr ptr; + MWScript::InterpreterContext interpreterContext ( - &iter->second.second, MWWorld::Ptr()); + &iter->second.mLocals, MWWorld::Ptr(), iter->second.mId); + MWBase::Environment::get().getScriptManager()->run (iter->first, interpreterContext); } } @@ -82,13 +90,32 @@ namespace MWScript void GlobalScripts::addStartup() { - addScript ("main"); + // make list of global scripts to be added + std::vector scripts; + + scripts.push_back ("main"); for (MWWorld::Store::iterator iter = mStore.get().begin(); iter != mStore.get().end(); ++iter) { - addScript (iter->mScript); + scripts.push_back (iter->mScript); + } + + // add scripts + for (std::vector::const_iterator iter (scripts.begin()); + iter!=scripts.end(); ++iter) + { + try + { + addScript (*iter); + } + catch (const std::exception& exception) + { + std::cerr + << "Failed to add start script " << *iter << " because an exception has " + << "been thrown: " << exception.what() << std::endl; + } } } @@ -99,16 +126,18 @@ namespace MWScript void GlobalScripts::write (ESM::ESMWriter& writer, Loading::Listener& progress) const { - for (std::map >::const_iterator iter (mScripts.begin()); + for (std::map::const_iterator iter (mScripts.begin()); iter!=mScripts.end(); ++iter) { ESM::GlobalScript script; script.mId = iter->first; - iter->second.second.write (script.mLocals, iter->first); + iter->second.mLocals.write (script.mLocals, iter->first); + + script.mRunning = iter->second.mRunning ? 1 : 0; - script.mRunning = iter->second.first ? 1 : 0; + script.mTargetId = iter->second.mId; writer.startRecord (ESM::REC_GSCR); script.save (writer); @@ -124,25 +153,37 @@ namespace MWScript ESM::GlobalScript script; script.load (reader); - std::map >::iterator iter = + std::map::iterator iter = mScripts.find (script.mId); if (iter==mScripts.end()) { if (const ESM::Script *scriptRecord = mStore.get().search (script.mId)) { - std::pair data (false, Locals()); - - data.second.configure (*scriptRecord); - - iter = mScripts.insert (std::make_pair (script.mId, data)).first; + try + { + GlobalScriptDesc desc; + desc.mLocals.configure (*scriptRecord); + + iter = mScripts.insert (std::make_pair (script.mId, desc)).first; + } + catch (const std::exception& exception) + { + std::cerr + << "Failed to add start script " << script.mId + << " because an exception has been thrown: " << exception.what() + << std::endl; + + return true; + } } else // script does not exist anymore return true; } - iter->second.first = script.mRunning!=0; - iter->second.second.read (script.mLocals, script.mId); + iter->second.mRunning = script.mRunning!=0; + iter->second.mLocals.read (script.mLocals, script.mId); + iter->second.mId = script.mTargetId; return true; } @@ -153,21 +194,19 @@ namespace MWScript Locals& GlobalScripts::getLocals (const std::string& name) { std::string name2 = ::Misc::StringUtils::lowerCase (name); - std::map >::iterator iter = - mScripts.find (name2); + std::map::iterator iter = mScripts.find (name2); if (iter==mScripts.end()) { if (const ESM::Script *script = mStore.get().find (name)) { - Locals locals; - - locals.configure (*script); + GlobalScriptDesc desc; + desc.mLocals.configure (*script); - iter = mScripts.insert (std::make_pair (name, std::make_pair (false, locals))).first; + iter = mScripts.insert (std::make_pair (name, desc)).first; } } - return iter->second.second; + return iter->second.mLocals; } } diff --git a/apps/openmw/mwscript/globalscripts.hpp b/apps/openmw/mwscript/globalscripts.hpp index 97584a5b8..55c2e9321 100644 --- a/apps/openmw/mwscript/globalscripts.hpp +++ b/apps/openmw/mwscript/globalscripts.hpp @@ -26,16 +26,25 @@ namespace MWWorld namespace MWScript { + struct GlobalScriptDesc + { + bool mRunning; + Locals mLocals; + std::string mId; // ID used to start targeted script (empty if not a targeted script) + + GlobalScriptDesc(); + }; + class GlobalScripts { const MWWorld::ESMStore& mStore; - std::map > mScripts; // running, local variables + std::map mScripts; public: GlobalScripts (const MWWorld::ESMStore& store); - void addScript (const std::string& name); + void addScript (const std::string& name, const std::string& targetId = ""); void removeScript (const std::string& name); diff --git a/apps/openmw/mwscript/guiextensions.cpp b/apps/openmw/mwscript/guiextensions.cpp index be241a564..afc745beb 100644 --- a/apps/openmw/mwscript/guiextensions.cpp +++ b/apps/openmw/mwscript/guiextensions.cpp @@ -162,6 +162,56 @@ namespace MWScript } }; + class OpMenuTest : public Interpreter::Opcode1 + { + public: + + virtual void execute (Interpreter::Runtime& runtime, unsigned int arg0) + { + int arg=0; + if(arg0>0) + { + arg = runtime[0].mInteger; + runtime.pop(); + } + + + if (arg == 0) + { + MWGui::GuiMode modes[] = { MWGui::GM_Inventory, MWGui::GM_Container }; + + for (int i=0; i<2; ++i) + { + if (MWBase::Environment::get().getWindowManager()->containsMode(modes[i])) + MWBase::Environment::get().getWindowManager()->removeGuiMode(modes[i]); + } + } + else + { + MWGui::GuiWindow gw = MWGui::GW_None; + if (arg == 3) + gw = MWGui::GW_Stats; + if (arg == 4) + gw = MWGui::GW_Inventory; + if (arg == 5) + gw = MWGui::GW_Magic; + if (arg == 6) + gw = MWGui::GW_Map; + + MWBase::Environment::get().getWindowManager()->pinWindow(gw); + } + } + }; + + class OpToggleMenus : public Interpreter::Opcode0 + { + public: + virtual void execute(Interpreter::Runtime &runtime) + { + bool state = MWBase::Environment::get().getWindowManager()->toggleGui(); + runtime.getContext().report(state ? "GUI -> On" : "GUI -> Off"); + } + }; void installOpcodes (Interpreter::Interpreter& interpreter) { @@ -200,6 +250,8 @@ namespace MWScript interpreter.installSegment5 (Compiler::Gui::opcodeShowMap, new OpShowMap); interpreter.installSegment5 (Compiler::Gui::opcodeFillMap, new OpFillMap); + interpreter.installSegment3 (Compiler::Gui::opcodeMenuTest, new OpMenuTest); + interpreter.installSegment5 (Compiler::Gui::opcodeToggleMenus, new OpToggleMenus); } } } diff --git a/apps/openmw/mwscript/interpretercontext.cpp b/apps/openmw/mwscript/interpretercontext.cpp index 6bf50371b..d8d13a921 100644 --- a/apps/openmw/mwscript/interpretercontext.cpp +++ b/apps/openmw/mwscript/interpretercontext.cpp @@ -3,8 +3,14 @@ #include #include +#include #include + +#include + +#include + #include "../mwworld/esmstore.hpp" #include "../mwbase/environment.hpp" @@ -14,6 +20,8 @@ #include "../mwbase/inputmanager.hpp" #include "../mwworld/class.hpp" +#include "../mwworld/cellstore.hpp" +#include "../mwworld/containerstore.hpp" #include "../mwmechanics/npcstats.hpp" @@ -22,7 +30,7 @@ namespace MWScript { - MWWorld::Ptr InterpreterContext::getReference ( + MWWorld::Ptr InterpreterContext::getReferenceImp ( const std::string& id, bool activeOnly, bool doThrow) { if (!id.empty()) @@ -31,6 +39,10 @@ namespace MWScript } else { + if (mReference.isEmpty() && !mTargetId.empty()) + mReference = + MWBase::Environment::get().getWorld()->searchPtr (mTargetId, false); + if (mReference.isEmpty() && doThrow) throw std::runtime_error ("no implicit reference"); @@ -38,7 +50,7 @@ namespace MWScript } } - const MWWorld::Ptr InterpreterContext::getReference ( + const MWWorld::Ptr InterpreterContext::getReferenceImp ( const std::string& id, bool activeOnly, bool doThrow) const { if (!id.empty()) @@ -47,6 +59,10 @@ namespace MWScript } else { + if (mReference.isEmpty() && !mTargetId.empty()) + mReference = + MWBase::Environment::get().getWorld()->searchPtr (mTargetId, false); + if (mReference.isEmpty() && doThrow) throw std::runtime_error ("no implicit reference"); @@ -64,7 +80,7 @@ namespace MWScript } else { - const MWWorld::Ptr ptr = getReference (id, false); + const MWWorld::Ptr ptr = getReferenceImp (id, false); id = ptr.getClass().getScript (ptr); @@ -84,7 +100,7 @@ namespace MWScript } else { - const MWWorld::Ptr ptr = getReference (id, false); + const MWWorld::Ptr ptr = getReferenceImp (id, false); id = ptr.getClass().getScript (ptr); @@ -95,11 +111,43 @@ namespace MWScript } } + int InterpreterContext::findLocalVariableIndex (const std::string& scriptId, + const std::string& name, char type) const + { + int index = MWBase::Environment::get().getScriptManager()->getLocals (scriptId). + searchIndex (type, name); + + if (index!=-1) + return index; + + std::ostringstream stream; + + stream << "Failed to access "; + + switch (type) + { + case 's': stream << "short"; break; + case 'l': stream << "long"; break; + case 'f': stream << "float"; break; + } + + stream << " member variable " << name << " in script " << scriptId; + + throw std::runtime_error (stream.str().c_str()); + } + + InterpreterContext::InterpreterContext ( - MWScript::Locals *locals, MWWorld::Ptr reference) + MWScript::Locals *locals, MWWorld::Ptr reference, const std::string& targetId) : mLocals (locals), mReference (reference), - mActivationHandled (false) - {} + mActivationHandled (false), mTargetId (targetId) + { + // If we run on a reference (local script, dialogue script or console with object + // selected), store the ID of that reference store it so it can be inherited by + // targeted scripts started from this one. + if (targetId.empty() && !reference.isEmpty()) + mTargetId = reference.getClass().getId (reference); + } int InterpreterContext::getLocalShort (int index) const { @@ -236,34 +284,37 @@ namespace MWScript std::string InterpreterContext::getNPCName() const { - ESM::NPC npc = *mReference.get()->mBase; + ESM::NPC npc = *getReferenceImp().get()->mBase; return npc.mName; } std::string InterpreterContext::getNPCRace() const { - ESM::NPC npc = *mReference.get()->mBase; + ESM::NPC npc = *getReferenceImp().get()->mBase; const ESM::Race* race = MWBase::Environment::get().getWorld()->getStore().get().find(npc.mRace); return race->mName; } std::string InterpreterContext::getNPCClass() const { - ESM::NPC npc = *mReference.get()->mBase; + ESM::NPC npc = *getReferenceImp().get()->mBase; const ESM::Class* class_ = MWBase::Environment::get().getWorld()->getStore().get().find(npc.mClass); return class_->mName; } std::string InterpreterContext::getNPCFaction() const { - ESM::NPC npc = *mReference.get()->mBase; + ESM::NPC npc = *getReferenceImp().get()->mBase; const ESM::Faction* faction = MWBase::Environment::get().getWorld()->getStore().get().find(npc.mFaction); return faction->mName; } std::string InterpreterContext::getNPCRank() const { - std::map ranks = mReference.getClass().getNpcStats (mReference).getFactionRanks(); + if (getReferenceImp().getClass().getNpcStats(getReferenceImp()).getFactionRanks().empty()) + throw std::runtime_error("getNPCRank(): NPC is not in a faction"); + + const std::map& ranks = getReferenceImp().getClass().getNpcStats (getReferenceImp()).getFactionRanks(); std::map::const_iterator it = ranks.begin(); MWBase::World *world = MWBase::Environment::get().getWorld(); @@ -299,9 +350,12 @@ namespace MWScript MWBase::World *world = MWBase::Environment::get().getWorld(); MWWorld::Ptr player = world->getPlayerPtr(); - std::string factionId = mReference.getClass().getNpcStats (mReference).getFactionRanks().begin()->first; + if (getReferenceImp().getClass().getNpcStats(getReferenceImp()).getFactionRanks().empty()) + throw std::runtime_error("getPCRank(): NPC is not in a faction"); - std::map ranks = player.getClass().getNpcStats (player).getFactionRanks(); + std::string factionId = getReferenceImp().getClass().getNpcStats (getReferenceImp()).getFactionRanks().begin()->first; + + const std::map& ranks = player.getClass().getNpcStats (player).getFactionRanks(); std::map::const_iterator it = ranks.find(factionId); int rank = -1; if (it != ranks.end()) @@ -326,9 +380,12 @@ namespace MWScript MWBase::World *world = MWBase::Environment::get().getWorld(); MWWorld::Ptr player = world->getPlayerPtr(); - std::string factionId = mReference.getClass().getNpcStats (mReference).getFactionRanks().begin()->first; + if (getReferenceImp().getClass().getNpcStats(getReferenceImp()).getFactionRanks().empty()) + throw std::runtime_error("getPCNextRank(): NPC is not in a faction"); + + std::string factionId = getReferenceImp().getClass().getNpcStats (getReferenceImp()).getFactionRanks().begin()->first; - std::map ranks = player.getClass().getNpcStats (player).getFactionRanks(); + const std::map& ranks = player.getClass().getNpcStats (player).getFactionRanks(); std::map::const_iterator it = ranks.find(factionId); int rank = -1; if (it != ranks.end()) @@ -366,9 +423,9 @@ namespace MWScript return MWBase::Environment::get().getScriptManager()->getGlobalScripts().isRunning (name); } - void InterpreterContext::startScript (const std::string& name) + void InterpreterContext::startScript (const std::string& name, const std::string& targetId) { - MWBase::Environment::get().getScriptManager()->getGlobalScripts().addScript (name); + MWBase::Environment::get().getScriptManager()->getGlobalScripts().addScript (name, targetId); } void InterpreterContext::stopScript (const std::string& name) @@ -378,14 +435,30 @@ namespace MWScript float InterpreterContext::getDistance (const std::string& name, const std::string& id) const { - const MWWorld::Ptr ref2 = getReference (id, false, false); - // If either actor is in a non-active cell, return a large value (just like vanilla) - if (ref2.isEmpty()) - return std::numeric_limits().max(); + // NOTE: id may be empty, indicating an implicit reference - const MWWorld::Ptr ref = getReference (name, false, false); - if (ref.isEmpty()) - return std::numeric_limits().max(); + MWWorld::Ptr ref2; + + if (id.empty()) + ref2 = getReferenceImp(); + else + ref2 = MWBase::Environment::get().getWorld()->getPtr(id, false); + + if (ref2.getContainerStore()) // is the object contained? + { + MWWorld::Ptr container = MWBase::Environment::get().getWorld()->findContainer(ref2); + + if (!container.isEmpty()) + ref2 = container; + else + throw std::runtime_error("failed to find container ptr"); + } + + const MWWorld::Ptr ref = MWBase::Environment::get().getWorld()->getPtr(name, false); + + // If the objects are in different worldspaces, return a large value (just like vanilla) + if (ref.getCell()->getCell()->getCellId().mWorldspace != ref2.getCell()->getCell()->getCellId().mWorldspace) + return std::numeric_limits::max(); double diff[3]; @@ -419,21 +492,14 @@ namespace MWScript mActivationHandled = false; } - void InterpreterContext::executeActivation(MWWorld::Ptr ptr) + void InterpreterContext::executeActivation(MWWorld::Ptr ptr, MWWorld::Ptr actor) { - MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); - boost::shared_ptr action = (ptr.getClass().activate(ptr, player)); - action->execute (player); + boost::shared_ptr action = (ptr.getClass().activate(ptr, actor)); + action->execute (actor); if (mActivated == ptr) mActivationHandled = true; } - void InterpreterContext::clearActivation() - { - mActivated = MWWorld::Ptr(); - mActivationHandled = false; - } - float InterpreterContext::getSecondsPassed() const { return MWBase::Environment::get().getFrameDuration(); @@ -441,19 +507,19 @@ namespace MWScript bool InterpreterContext::isDisabled (const std::string& id) const { - const MWWorld::Ptr ref = getReference (id, false); + const MWWorld::Ptr ref = getReferenceImp (id, false); return !ref.getRefData().isEnabled(); } void InterpreterContext::enable (const std::string& id) { - MWWorld::Ptr ref = getReference (id, false); + MWWorld::Ptr ref = getReferenceImp (id, false); MWBase::Environment::get().getWorld()->enable (ref); } void InterpreterContext::disable (const std::string& id) { - MWWorld::Ptr ref = getReference (id, false); + MWWorld::Ptr ref = getReferenceImp (id, false); MWBase::Environment::get().getWorld()->disable (ref); } @@ -464,10 +530,7 @@ namespace MWScript const Locals& locals = getMemberLocals (scriptId, global); - int index = MWBase::Environment::get().getScriptManager()->getLocalIndex ( - scriptId, name, 's'); - - return locals.mShorts[index]; + return locals.mShorts[findLocalVariableIndex (scriptId, name, 's')]; } int InterpreterContext::getMemberLong (const std::string& id, const std::string& name, @@ -477,10 +540,7 @@ namespace MWScript const Locals& locals = getMemberLocals (scriptId, global); - int index = MWBase::Environment::get().getScriptManager()->getLocalIndex ( - scriptId, name, 'l'); - - return locals.mLongs[index]; + return locals.mLongs[findLocalVariableIndex (scriptId, name, 'l')]; } float InterpreterContext::getMemberFloat (const std::string& id, const std::string& name, @@ -490,10 +550,7 @@ namespace MWScript const Locals& locals = getMemberLocals (scriptId, global); - int index = MWBase::Environment::get().getScriptManager()->getLocalIndex ( - scriptId, name, 'f'); - - return locals.mFloats[index]; + return locals.mFloats[findLocalVariableIndex (scriptId, name, 'f')]; } void InterpreterContext::setMemberShort (const std::string& id, const std::string& name, @@ -503,10 +560,7 @@ namespace MWScript Locals& locals = getMemberLocals (scriptId, global); - int index = - MWBase::Environment::get().getScriptManager()->getLocalIndex (scriptId, name, 's'); - - locals.mShorts[index] = value; + locals.mShorts[findLocalVariableIndex (scriptId, name, 's')] = value; } void InterpreterContext::setMemberLong (const std::string& id, const std::string& name, int value, bool global) @@ -515,10 +569,7 @@ namespace MWScript Locals& locals = getMemberLocals (scriptId, global); - int index = - MWBase::Environment::get().getScriptManager()->getLocalIndex (scriptId, name, 'l'); - - locals.mLongs[index] = value; + locals.mLongs[findLocalVariableIndex (scriptId, name, 'l')] = value; } void InterpreterContext::setMemberFloat (const std::string& id, const std::string& name, float value, bool global) @@ -527,14 +578,16 @@ namespace MWScript Locals& locals = getMemberLocals (scriptId, global); - int index = - MWBase::Environment::get().getScriptManager()->getLocalIndex (scriptId, name, 'f'); - - locals.mFloats[index] = value; + locals.mFloats[findLocalVariableIndex (scriptId, name, 'f')] = value; } MWWorld::Ptr InterpreterContext::getReference(bool required) { - return getReference ("", true, required); + return getReferenceImp ("", true, required); + } + + std::string InterpreterContext::getTargetId() const + { + return mTargetId; } } diff --git a/apps/openmw/mwscript/interpretercontext.hpp b/apps/openmw/mwscript/interpretercontext.hpp index 1137efed3..b54339965 100644 --- a/apps/openmw/mwscript/interpretercontext.hpp +++ b/apps/openmw/mwscript/interpretercontext.hpp @@ -27,14 +27,22 @@ namespace MWScript class InterpreterContext : public Interpreter::Context { Locals *mLocals; - MWWorld::Ptr mReference; + mutable MWWorld::Ptr mReference; MWWorld::Ptr mActivated; bool mActivationHandled; - MWWorld::Ptr getReference (const std::string& id, bool activeOnly, bool doThrow=true); + std::string mTargetId; - const MWWorld::Ptr getReference (const std::string& id, bool activeOnly, bool doThrow=true) const; + /// If \a id is empty, a reference the script is run from is returned or in case + /// of a non-local script the reference derived from the target ID. + MWWorld::Ptr getReferenceImp (const std::string& id = "", bool activeOnly = false, + bool doThrow=true); + + /// If \a id is empty, a reference the script is run from is returned or in case + /// of a non-local script the reference derived from the target ID. + const MWWorld::Ptr getReferenceImp (const std::string& id = "", + bool activeOnly = false, bool doThrow=true) const; const Locals& getMemberLocals (std::string& id, bool global) const; ///< \a id is changed to the respective script ID, if \a id wasn't a script ID before @@ -42,9 +50,14 @@ namespace MWScript Locals& getMemberLocals (std::string& id, bool global); ///< \a id is changed to the respective script ID, if \a id wasn't a script ID before + /// Throws an exception if local variable can't be found. + int findLocalVariableIndex (const std::string& scriptId, const std::string& name, + char type) const; + public: - InterpreterContext (MWScript::Locals *locals, MWWorld::Ptr reference); + InterpreterContext (MWScript::Locals *locals, MWWorld::Ptr reference, + const std::string& targetId = ""); ///< The ownership of \a locals is not transferred. 0-pointer allowed. virtual int getLocalShort (int index) const; @@ -113,11 +126,12 @@ namespace MWScript virtual bool isScriptRunning (const std::string& name) const; - virtual void startScript (const std::string& name); + virtual void startScript (const std::string& name, const std::string& targetId = ""); virtual void stopScript (const std::string& name); virtual float getDistance (const std::string& name, const std::string& id = "") const; + ///< @note if \a id is empty, assumes an implicit reference bool hasBeenActivated (const MWWorld::Ptr& ptr); ///< \attention Calling this function for the right reference will mark the action as @@ -129,12 +143,9 @@ namespace MWScript ///< Store reference acted upon. The actual execution of the action does not /// take place here. - void executeActivation(MWWorld::Ptr ptr); + void executeActivation(MWWorld::Ptr ptr, MWWorld::Ptr actor); ///< Execute the activation action for this ptr. If ptr is mActivated, mark activation as handled. - void clearActivation(); - ///< Discard the action defined by the last activate call. - virtual float getSecondsPassed() const; virtual bool isDisabled (const std::string& id = "") const; @@ -157,6 +168,8 @@ namespace MWScript MWWorld::Ptr getReference(bool required=true); ///< Reference, that the script is running from (can be empty) + + virtual std::string getTargetId() const; }; } diff --git a/apps/openmw/mwscript/locals.cpp b/apps/openmw/mwscript/locals.cpp index 094fe085a..a1ee48ae6 100644 --- a/apps/openmw/mwscript/locals.cpp +++ b/apps/openmw/mwscript/locals.cpp @@ -5,6 +5,7 @@ #include #include +#include #include "../mwbase/environment.hpp" #include "../mwbase/scriptmanager.hpp" @@ -13,17 +14,25 @@ namespace MWScript { void Locals::configure (const ESM::Script& script) { + const Compiler::Locals& locals = + MWBase::Environment::get().getScriptManager()->getLocals (script.mId); + mShorts.clear(); - mShorts.resize (script.mData.mNumShorts, 0); + mShorts.resize (locals.get ('s').size(), 0); mLongs.clear(); - mLongs.resize (script.mData.mNumLongs, 0); + mLongs.resize (locals.get ('l').size(), 0); mFloats.clear(); - mFloats.resize (script.mData.mNumFloats, 0); + mFloats.resize (locals.get ('f').size(), 0); + } + + bool Locals::isEmpty() const + { + return (mShorts.empty() && mLongs.empty() && mFloats.empty()); } int Locals::getIntVar(const std::string &script, const std::string &var) { - Compiler::Locals locals = MWBase::Environment::get().getScriptManager()->getLocals(script); + const Compiler::Locals& locals = MWBase::Environment::get().getScriptManager()->getLocals(script); int index = locals.getIndex(var); char type = locals.getType(var); if(index != -1) @@ -47,7 +56,7 @@ namespace MWScript bool Locals::setVarByInt(const std::string& script, const std::string& var, int val) { - Compiler::Locals locals = MWBase::Environment::get().getScriptManager()->getLocals(script); + const Compiler::Locals& locals = MWBase::Environment::get().getScriptManager()->getLocals(script); int index = locals.getIndex(var); char type = locals.getType(var); if(index != -1) @@ -70,65 +79,77 @@ namespace MWScript void Locals::write (ESM::Locals& locals, const std::string& script) const { - const Compiler::Locals& declarations = - MWBase::Environment::get().getScriptManager()->getLocals(script); - - for (int i=0; i<3; ++i) + try { - char type = 0; + const Compiler::Locals& declarations = + MWBase::Environment::get().getScriptManager()->getLocals(script); - switch (i) + for (int i=0; i<3; ++i) { - case 0: type = 's'; break; - case 1: type = 'l'; break; - case 2: type = 'f'; break; - } - - const std::vector& names = declarations.get (type); - - for (int i2=0; i2 (names.size()); ++i2) - { - ESM::Variant value; + char type = 0; switch (i) { - case 0: value.setType (ESM::VT_Int); value.setInteger (mShorts.at (i2)); break; - case 1: value.setType (ESM::VT_Int); value.setInteger (mLongs.at (i2)); break; - case 2: value.setType (ESM::VT_Float); value.setFloat (mFloats.at (i2)); break; + case 0: type = 's'; break; + case 1: type = 'l'; break; + case 2: type = 'f'; break; } - locals.mVariables.push_back (std::make_pair (names[i2], value)); + const std::vector& names = declarations.get (type); + + for (int i2=0; i2 (names.size()); ++i2) + { + ESM::Variant value; + + switch (i) + { + case 0: value.setType (ESM::VT_Int); value.setInteger (mShorts.at (i2)); break; + case 1: value.setType (ESM::VT_Int); value.setInteger (mLongs.at (i2)); break; + case 2: value.setType (ESM::VT_Float); value.setFloat (mFloats.at (i2)); break; + } + + locals.mVariables.push_back (std::make_pair (names[i2], value)); + } } } + catch (const Compiler::SourceException&) + { + } } void Locals::read (const ESM::Locals& locals, const std::string& script) { - const Compiler::Locals& declarations = - MWBase::Environment::get().getScriptManager()->getLocals(script); - - for (std::vector >::const_iterator iter - = locals.mVariables.begin(); iter!=locals.mVariables.end(); ++iter) + try { - char type = declarations.getType (iter->first); - char index = declarations.getIndex (iter->first); + const Compiler::Locals& declarations = + MWBase::Environment::get().getScriptManager()->getLocals(script); - try + for (std::vector >::const_iterator iter + = locals.mVariables.begin(); iter!=locals.mVariables.end(); ++iter) { - switch (type) - { - case 's': mShorts.at (index) = iter->second.getInteger(); break; - case 'l': mLongs.at (index) = iter->second.getInteger(); break; - case 'f': mFloats.at (index) = iter->second.getFloat(); break; + char type = declarations.getType (iter->first); + char index = declarations.getIndex (iter->first); - // silently ignore locals that don't exist anymore + try + { + switch (type) + { + case 's': mShorts.at (index) = iter->second.getInteger(); break; + case 'l': mLongs.at (index) = iter->second.getInteger(); break; + case 'f': mFloats.at (index) = iter->second.getFloat(); break; + + // silently ignore locals that don't exist anymore + } + } + catch (...) + { + // ignore type changes + /// \todo write to log } } - catch (...) - { - // ignore type changes - /// \todo write to log - } + } + catch (const Compiler::SourceException&) + { } } } diff --git a/apps/openmw/mwscript/locals.hpp b/apps/openmw/mwscript/locals.hpp index d17d1be2d..9fa4214ac 100644 --- a/apps/openmw/mwscript/locals.hpp +++ b/apps/openmw/mwscript/locals.hpp @@ -20,9 +20,15 @@ namespace MWScript std::vector mLongs; std::vector mFloats; + /// Are there any locals? + bool isEmpty() const; + void configure (const ESM::Script& script); + /// @note var needs to be in lowercase bool setVarByInt(const std::string& script, const std::string& var, int val); - int getIntVar (const std::string& script, const std::string& var); ///< if var does not exist, returns 0 + int getIntVar (const std::string& script, const std::string& var); + ///< if var does not exist, returns 0 + /// @note var needs to be in lowercase void write (ESM::Locals& locals, const std::string& script) const; diff --git a/apps/openmw/mwscript/miscextensions.cpp b/apps/openmw/mwscript/miscextensions.cpp index 9d6d5e50d..c7d221139 100644 --- a/apps/openmw/mwscript/miscextensions.cpp +++ b/apps/openmw/mwscript/miscextensions.cpp @@ -3,8 +3,6 @@ #include -#include - #include #include #include @@ -24,6 +22,7 @@ #include "../mwworld/player.hpp" #include "../mwworld/containerstore.hpp" #include "../mwworld/esmstore.hpp" +#include "../mwworld/cellstore.hpp" #include "../mwmechanics/npcstats.hpp" #include "../mwmechanics/creaturestats.hpp" @@ -121,7 +120,7 @@ namespace MWScript MWWorld::Ptr ptr = R()(runtime); - context.executeActivation(ptr); + context.executeActivation(ptr, MWBase::Environment::get().getWorld()->getPlayerPtr()); } }; @@ -146,6 +145,14 @@ namespace MWScript } ptr.getClass().lock (ptr, lockLevel); + + // Instantly reset door to closed state + // This is done when using Lock in scripts, but not when using Lock spells. + if (ptr.getTypeName() == typeid(ESM::Door).name() && !ptr.getCellRef().getTeleport()) + { + MWBase::Environment::get().getWorld()->activateDoor(ptr, 0); + MWBase::Environment::get().getWorld()->localRotateObject(ptr, 0, 0, 0); + } } }; @@ -239,7 +246,7 @@ namespace MWScript Interpreter::Type_Float time = runtime[0].mFloat; runtime.pop(); - MWBase::Environment::get().getWorld()->getFader()->fadeIn(time); + MWBase::Environment::get().getWindowManager()->fadeScreenIn(time); } }; @@ -252,7 +259,7 @@ namespace MWScript Interpreter::Type_Float time = runtime[0].mFloat; runtime.pop(); - MWBase::Environment::get().getWorld()->getFader()->fadeOut(time); + MWBase::Environment::get().getWindowManager()->fadeScreenOut(time); } }; @@ -268,7 +275,7 @@ namespace MWScript Interpreter::Type_Float time = runtime[0].mFloat; runtime.pop(); - MWBase::Environment::get().getWorld()->getFader()->fadeTo(alpha, time); + MWBase::Environment::get().getWindowManager()->fadeScreenTo(alpha, time); } }; @@ -283,6 +290,17 @@ namespace MWScript } }; + class OpToggleWorld : public Interpreter::Opcode0 + { + public: + + virtual void execute (Interpreter::Runtime& runtime) + { + runtime.getContext().report(MWBase::Environment::get().getWorld()->toggleWorld() ? "World -> On" + : "World -> Off"); + } + }; + class OpDontSaveObject : public Interpreter::Opcode0 { public: @@ -349,7 +367,7 @@ namespace MWScript key = ESM::MagicEffect::effectStringToId(effect); runtime.push(ptr.getClass().getCreatureStats(ptr).getMagicEffects().get( - MWMechanics::EffectKey(key)).mMagnitude > 0); + MWMechanics::EffectKey(key)).getMagnitude() > 0); } }; @@ -436,7 +454,8 @@ namespace MWScript if (::Misc::StringUtils::ciEqual(iter->getCellRef().getRefId(), item)) { int removed = store.remove(*iter, toRemove, ptr); - MWBase::Environment::get().getWorld()->dropObjectOnGround(ptr, *iter, removed); + MWWorld::Ptr dropped = MWBase::Environment::get().getWorld()->dropObjectOnGround(ptr, *iter, removed); + dropped.getCellRef().setOwner(""); toRemove -= removed; @@ -602,6 +621,60 @@ namespace MWScript } }; + template + class OpGetCollidingPc : public Interpreter::Opcode0 + { + public: + + virtual void execute (Interpreter::Runtime& runtime) + { + MWWorld::Ptr ptr = R()(runtime); + runtime.push (MWBase::Environment::get().getWorld()->getPlayerCollidingWith(ptr)); + } + }; + + template + class OpGetCollidingActor : public Interpreter::Opcode0 + { + public: + + virtual void execute (Interpreter::Runtime& runtime) + { + MWWorld::Ptr ptr = R()(runtime); + runtime.push (MWBase::Environment::get().getWorld()->getActorCollidingWith(ptr)); + } + }; + + template + class OpHurtStandingActor : public Interpreter::Opcode0 + { + public: + + virtual void execute (Interpreter::Runtime& runtime) + { + MWWorld::Ptr ptr = R()(runtime); + float healthDiffPerSecond = runtime[0].mFloat; + runtime.pop(); + + MWBase::Environment::get().getWorld()->hurtStandingActors(ptr, healthDiffPerSecond); + } + }; + + template + class OpHurtCollidingActor : public Interpreter::Opcode0 + { + public: + + virtual void execute (Interpreter::Runtime& runtime) + { + MWWorld::Ptr ptr = R()(runtime); + float healthDiffPerSecond = runtime[0].mFloat; + runtime.pop(); + + MWBase::Environment::get().getWorld()->hurtCollidingActors(ptr, healthDiffPerSecond); + } + }; + class OpGetWindSpeed : public Interpreter::Opcode0 { public: @@ -782,6 +855,7 @@ namespace MWScript MWMechanics::CastSpell cast(ptr, target); cast.mHitPosition = Ogre::Vector3(target.getRefData().getPosition().pos); + cast.mAlwaysSucceed = true; cast.cast(spell); } }; @@ -799,6 +873,7 @@ namespace MWScript MWMechanics::CastSpell cast(ptr, ptr); cast.mHitPosition = Ogre::Vector3(ptr.getRefData().getPosition().pos); + cast.mAlwaysSucceed = true; cast.cast(spell); } }; @@ -810,7 +885,6 @@ namespace MWScript { MWBase::World* world = MWBase::Environment::get().getWorld(); world->goToJail(); - MWBase::Environment::get().getWorld()->getPlayer().recordCrimeId(); } }; @@ -859,6 +933,48 @@ namespace MWScript } }; + template + class OpBetaComment : public Interpreter::Opcode0 + { + public: + virtual void execute(Interpreter::Runtime &runtime) + { + MWWorld::Ptr ptr = R()(runtime); + + std::stringstream msg; + + msg << "Content file: "; + + if (ptr.getCellRef().getRefNum().mContentFile == -1) + msg << "[None]" << std::endl; + else + { + std::vector contentFiles = MWBase::Environment::get().getWorld()->getContentFiles(); + + msg << contentFiles.at (ptr.getCellRef().getRefNum().mContentFile) << std::endl; + } + + msg << "RefID: " << ptr.getCellRef().getRefId() << std::endl; + + if (ptr.isInCell()) + { + MWWorld::CellStore* cell = ptr.getCell(); + msg << "Cell: " << MWBase::Environment::get().getWorld()->getCellName(cell) << std::endl; + if (cell->getCell()->isExterior()) + msg << "Grid: " << cell->getCell()->getGridX() << " " << cell->getCell()->getGridY() << std::endl; + Ogre::Vector3 pos (ptr.getRefData().getPosition().pos); + msg << "Coordinates: " << pos << std::endl; + } + + std::string notes = runtime.getStringLiteral (runtime[0].mInteger); + runtime.pop(); + if (!notes.empty()) + msg << "Notes: " << notes << std::endl; + + runtime.getContext().report(msg.str()); + } + }; + void installOpcodes (Interpreter::Interpreter& interpreter) { interpreter.installSegment5 (Compiler::Misc::opcodeXBox, new OpXBox); @@ -877,6 +993,7 @@ namespace MWScript interpreter.installSegment5 (Compiler::Misc::opcodeFadeTo, new OpFadeTo); interpreter.installSegment5 (Compiler::Misc::opcodeTogglePathgrid, new OpTogglePathgrid); interpreter.installSegment5 (Compiler::Misc::opcodeToggleWater, new OpToggleWater); + interpreter.installSegment5 (Compiler::Misc::opcodeToggleWorld, new OpToggleWorld); interpreter.installSegment5 (Compiler::Misc::opcodeDontSaveObject, new OpDontSaveObject); interpreter.installSegment5 (Compiler::Misc::opcodeToggleVanityMode, new OpToggleVanityMode); interpreter.installSegment5 (Compiler::Misc::opcodeGetPcSleep, new OpGetPcSleep); @@ -916,6 +1033,14 @@ namespace MWScript interpreter.installSegment5 (Compiler::Misc::opcodeGetStandingPcExplicit, new OpGetStandingPc); interpreter.installSegment5 (Compiler::Misc::opcodeGetStandingActor, new OpGetStandingActor); interpreter.installSegment5 (Compiler::Misc::opcodeGetStandingActorExplicit, new OpGetStandingActor); + interpreter.installSegment5 (Compiler::Misc::opcodeGetCollidingPc, new OpGetCollidingPc); + interpreter.installSegment5 (Compiler::Misc::opcodeGetCollidingPcExplicit, new OpGetCollidingPc); + interpreter.installSegment5 (Compiler::Misc::opcodeGetCollidingActor, new OpGetCollidingActor); + interpreter.installSegment5 (Compiler::Misc::opcodeGetCollidingActorExplicit, new OpGetCollidingActor); + interpreter.installSegment5 (Compiler::Misc::opcodeHurtStandingActor, new OpHurtStandingActor); + interpreter.installSegment5 (Compiler::Misc::opcodeHurtStandingActorExplicit, new OpHurtStandingActor); + interpreter.installSegment5 (Compiler::Misc::opcodeHurtCollidingActor, new OpHurtCollidingActor); + interpreter.installSegment5 (Compiler::Misc::opcodeHurtCollidingActorExplicit, new OpHurtCollidingActor); interpreter.installSegment5 (Compiler::Misc::opcodeGetWindSpeed, new OpGetWindSpeed); interpreter.installSegment5 (Compiler::Misc::opcodeHitOnMe, new OpHitOnMe); interpreter.installSegment5 (Compiler::Misc::opcodeHitOnMeExplicit, new OpHitOnMe); @@ -932,6 +1057,8 @@ namespace MWScript interpreter.installSegment5 (Compiler::Misc::opcodeExplodeSpellExplicit, new OpExplodeSpell); interpreter.installSegment5 (Compiler::Misc::opcodeGetPcInJail, new OpGetPcInJail); interpreter.installSegment5 (Compiler::Misc::opcodeGetPcTraveling, new OpGetPcTraveling); + interpreter.installSegment5 (Compiler::Misc::opcodeBetaComment, new OpBetaComment); + interpreter.installSegment5 (Compiler::Misc::opcodeBetaCommentExplicit, new OpBetaComment); } } } diff --git a/apps/openmw/mwscript/ref.hpp b/apps/openmw/mwscript/ref.hpp index 795839139..18f7453e4 100644 --- a/apps/openmw/mwscript/ref.hpp +++ b/apps/openmw/mwscript/ref.hpp @@ -16,18 +16,27 @@ namespace MWScript { struct ExplicitRef { - MWWorld::Ptr operator() (Interpreter::Runtime& runtime, bool required=true) const + static const bool implicit = false; + + MWWorld::Ptr operator() (Interpreter::Runtime& runtime, bool required=true, + bool activeOnly = false) const { std::string id = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); - return MWBase::Environment::get().getWorld()->getPtr (id, false); + if (required) + return MWBase::Environment::get().getWorld()->getPtr (id, activeOnly); + else + return MWBase::Environment::get().getWorld()->searchPtr (id, activeOnly); } }; struct ImplicitRef { - MWWorld::Ptr operator() (Interpreter::Runtime& runtime, bool required=true) const + static const bool implicit = true; + + MWWorld::Ptr operator() (Interpreter::Runtime& runtime, bool required=true, + bool activeOnly = false) const { MWScript::InterpreterContext& context = static_cast (runtime.getContext()); diff --git a/apps/openmw/mwscript/scriptmanagerimp.cpp b/apps/openmw/mwscript/scriptmanagerimp.cpp index 74c85dbbf..5f755e542 100644 --- a/apps/openmw/mwscript/scriptmanagerimp.cpp +++ b/apps/openmw/mwscript/scriptmanagerimp.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include @@ -22,12 +23,19 @@ namespace MWScript { ScriptManager::ScriptManager (const MWWorld::ESMStore& store, bool verbose, - Compiler::Context& compilerContext, int warningsMode) + Compiler::Context& compilerContext, int warningsMode, + const std::vector& scriptBlacklist) : mErrorHandler (std::cerr), mStore (store), mVerbose (verbose), mCompilerContext (compilerContext), mParser (mErrorHandler, mCompilerContext), mOpcodesInstalled (false), mGlobalScripts (store) { mErrorHandler.setWarningsMode (warningsMode); + + mScriptBlacklist.resize (scriptBlacklist.size()); + + std::transform (scriptBlacklist.begin(), scriptBlacklist.end(), + mScriptBlacklist.begin(), Misc::StringUtils::lowerCase); + std::sort (mScriptBlacklist.begin(), mScriptBlacklist.end()); } bool ScriptManager::compile (const std::string& name) @@ -35,13 +43,12 @@ namespace MWScript mParser.reset(); mErrorHandler.reset(); - bool Success = true; - if (const ESM::Script *script = mStore.get().find (name)) { if (mVerbose) std::cout << "compiling script: " << name << std::endl; + bool Success = true; try { std::istringstream input (script->mScriptText); @@ -64,12 +71,12 @@ namespace MWScript Success = false; } - if (!Success && mVerbose) + if (!Success) { std::cerr - << "compiling failed: " << name << std::endl - << script->mScriptText - << std::endl << std::endl; + << "compiling failed: " << name << std::endl; + if (mVerbose) + std::cerr << script->mScriptText << std::endl << std::endl; } if (Success) @@ -78,8 +85,6 @@ namespace MWScript mParser.getCode (code); mScripts.insert (std::make_pair (name, std::make_pair (code, mParser.getLocals()))); - // TODO sanity check on generated locals - return true; } } @@ -133,16 +138,22 @@ namespace MWScript int success = 0; const MWWorld::Store& scripts = mStore.get(); - MWWorld::Store::iterator it = scripts.begin(); - for (; it != scripts.end(); ++it, ++count) - if (compile (it->mId)) - ++success; + for (MWWorld::Store::iterator iter = scripts.begin(); + iter != scripts.end(); ++iter) + if (!std::binary_search (mScriptBlacklist.begin(), mScriptBlacklist.end(), + Misc::StringUtils::lowerCase (iter->mId))) + { + ++count; + + if (compile (iter->mId)) + ++success; + } return std::make_pair (count, success); } - Compiler::Locals& ScriptManager::getLocals (const std::string& name) + const Compiler::Locals& ScriptManager::getLocals (const std::string& name) { std::string name2 = Misc::StringUtils::lowerCase (name); @@ -162,6 +173,11 @@ namespace MWScript if (const ESM::Script *script = mStore.get().find (name2)) { + if (mVerbose) + std::cout + << "scanning script for local variable declarations: " << name2 + << std::endl; + Compiler::Locals locals; std::istringstream stream (script->mScriptText); @@ -182,46 +198,4 @@ namespace MWScript { return mGlobalScripts; } - - int ScriptManager::getLocalIndex (const std::string& scriptId, const std::string& variable, - char type) - { - const ESM::Script *script = mStore.get().find (scriptId); - - int offset = 0; - int size = 0; - - switch (type) - { - case 's': - - offset = 0; - size = script->mData.mNumShorts; - break; - - case 'l': - - offset = script->mData.mNumShorts; - size = script->mData.mNumLongs; - break; - - case 'f': - - offset = script->mData.mNumShorts+script->mData.mNumLongs; - size = script->mData.mNumFloats; - break; - - default: - - throw std::runtime_error ("invalid variable type"); - } - - std::string variable2 = Misc::StringUtils::lowerCase (variable); - - for (int i=0; imVarNames.at (i+offset))==variable2) - return i; - - throw std::runtime_error ("unable to access local variable " + variable + " of " + scriptId); - } } diff --git a/apps/openmw/mwscript/scriptmanagerimp.hpp b/apps/openmw/mwscript/scriptmanagerimp.hpp index da3abc60b..6026f6aba 100644 --- a/apps/openmw/mwscript/scriptmanagerimp.hpp +++ b/apps/openmw/mwscript/scriptmanagerimp.hpp @@ -48,11 +48,13 @@ namespace MWScript ScriptCollection mScripts; GlobalScripts mGlobalScripts; std::map mOtherLocals; + std::vector mScriptBlacklist; public: ScriptManager (const MWWorld::ESMStore& store, bool verbose, - Compiler::Context& compilerContext, int warningsMode); + Compiler::Context& compilerContext, int warningsMode, + const std::vector& scriptBlacklist); virtual void run (const std::string& name, Interpreter::Context& interpreterContext); ///< Run the script with the given name (compile first, if not compiled yet) @@ -65,15 +67,10 @@ namespace MWScript ///< Compile all scripts /// \return count, success - virtual Compiler::Locals& getLocals (const std::string& name); + virtual const Compiler::Locals& getLocals (const std::string& name); ///< Return locals for script \a name. virtual GlobalScripts& getGlobalScripts(); - - virtual int getLocalIndex (const std::string& scriptId, const std::string& variable, - char type); - ///< Return index of the variable of the given name and type in the given script. Will - /// throw an exception, if there is no such script or variable or the type does not match. }; } diff --git a/apps/openmw/mwscript/statsextensions.cpp b/apps/openmw/mwscript/statsextensions.cpp index 8b61237a1..befb8d82e 100644 --- a/apps/openmw/mwscript/statsextensions.cpp +++ b/apps/openmw/mwscript/statsextensions.cpp @@ -20,6 +20,7 @@ #include "../mwbase/windowmanager.hpp" #include "../mwworld/class.hpp" +#include "../mwworld/player.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/npcstats.hpp" @@ -217,11 +218,28 @@ namespace MWScript virtual void execute (Interpreter::Runtime& runtime) { + int peek = R::implicit ? 0 : runtime[0].mInteger; + MWWorld::Ptr ptr = R()(runtime); Interpreter::Type_Float diff = runtime[0].mFloat; runtime.pop(); + // workaround broken endgame scripts that kill dagoth ur + if (!R::implicit && + Misc::StringUtils::ciEqual(ptr.getCellRef().getRefId(), "dagoth_ur_1")) + { + runtime.push (peek); + + if (R()(runtime, false, true).isEmpty()) + { + std::cerr + << "Compensating for broken script in Morrowind.esm by " + << "ignoring remote access to dagoth_ur_1" << std::endl; + return; + } + } + MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats (ptr); Interpreter::Type_Float current = stats.getDynamic(mIndex).getCurrent(); @@ -260,7 +278,7 @@ namespace MWScript MWMechanics::DynamicStat stat (ptr.getClass().getCreatureStats (ptr) .getDynamic (mIndex)); - stat.setCurrent (diff + current); + stat.setCurrent (diff + current, true); ptr.getClass().getCreatureStats (ptr).setDynamic (mIndex, stat); } @@ -399,8 +417,12 @@ namespace MWScript MWBase::World *world = MWBase::Environment::get().getWorld(); MWWorld::Ptr player = world->getPlayerPtr(); - player.getClass().getNpcStats (player).setBounty(runtime[0].mFloat); + int bounty = runtime[0].mFloat; runtime.pop(); + player.getClass().getNpcStats (player).setBounty(bounty); + + if (bounty == 0) + MWBase::Environment::get().getWorld()->getPlayer().recordCrimeId(); } }; @@ -528,11 +550,12 @@ namespace MWScript virtual void execute (Interpreter::Runtime& runtime, unsigned int arg0) { + MWWorld::Ptr actor = R()(runtime, false); + std::string factionID = ""; if(arg0==0) { - MWWorld::Ptr actor = R()(runtime); factionID = getDialogueActorFaction(actor); } else @@ -547,10 +570,7 @@ namespace MWScript if(factionID != "") { MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); - if(player.getClass().getNpcStats(player).getFactionRanks().find(factionID) == player.getClass().getNpcStats(player).getFactionRanks().end()) - { - player.getClass().getNpcStats(player).getFactionRanks()[factionID] = 0; - } + player.getClass().getNpcStats(player).joinFaction(factionID); } } }; @@ -562,11 +582,12 @@ namespace MWScript virtual void execute (Interpreter::Runtime& runtime, unsigned int arg0) { + MWWorld::Ptr actor = R()(runtime, false); + std::string factionID = ""; if(arg0==0) { - MWWorld::Ptr actor = R()(runtime); factionID = getDialogueActorFaction(actor); } else @@ -583,11 +604,11 @@ namespace MWScript MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); if(player.getClass().getNpcStats(player).getFactionRanks().find(factionID) == player.getClass().getNpcStats(player).getFactionRanks().end()) { - player.getClass().getNpcStats(player).getFactionRanks()[factionID] = 0; + player.getClass().getNpcStats(player).joinFaction(factionID); } else { - player.getClass().getNpcStats(player).getFactionRanks()[factionID] = player.getClass().getNpcStats(player).getFactionRanks()[factionID] +1; + player.getClass().getNpcStats(player).raiseRank(factionID); } } } @@ -600,11 +621,12 @@ namespace MWScript virtual void execute (Interpreter::Runtime& runtime, unsigned int arg0) { + MWWorld::Ptr actor = R()(runtime, false); + std::string factionID = ""; if(arg0==0) { - MWWorld::Ptr actor = R()(runtime); factionID = getDialogueActorFaction(actor); } else @@ -619,10 +641,7 @@ namespace MWScript if(factionID != "") { MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); - if(player.getClass().getNpcStats(player).getFactionRanks().find(factionID) != player.getClass().getNpcStats(player).getFactionRanks().end()) - { - player.getClass().getNpcStats(player).getFactionRanks()[factionID] = player.getClass().getNpcStats(player).getFactionRanks()[factionID] -1; - } + player.getClass().getNpcStats(player).lowerRank(factionID); } } }; @@ -634,7 +653,7 @@ namespace MWScript virtual void execute (Interpreter::Runtime& runtime, unsigned int arg0) { - MWWorld::Ptr ptr = R()(runtime); + MWWorld::Ptr ptr = R()(runtime, false); std::string factionID = ""; if(arg0 >0) @@ -662,7 +681,7 @@ namespace MWScript { if(player.getClass().getNpcStats(player).getFactionRanks().find(factionID) != player.getClass().getNpcStats(player).getFactionRanks().end()) { - runtime.push(player.getClass().getNpcStats(player).getFactionRanks()[factionID]); + runtime.push(player.getClass().getNpcStats(player).getFactionRanks().at(factionID)); } else { @@ -688,8 +707,11 @@ namespace MWScript Interpreter::Type_Integer value = runtime[0].mInteger; runtime.pop(); - ptr.getClass().getNpcStats (ptr).setBaseDisposition - (ptr.getClass().getNpcStats (ptr).getBaseDisposition() + value); + if (ptr.getClass().isNpc()) + ptr.getClass().getNpcStats (ptr).setBaseDisposition + (ptr.getClass().getNpcStats (ptr).getBaseDisposition() + value); + + // else: must not throw exception (used by an Almalexia dialogue script) } }; @@ -705,7 +727,8 @@ namespace MWScript Interpreter::Type_Integer value = runtime[0].mInteger; runtime.pop(); - ptr.getClass().getNpcStats (ptr).setBaseDisposition (value); + if (ptr.getClass().isNpc()) + ptr.getClass().getNpcStats (ptr).setBaseDisposition (value); } }; @@ -718,7 +741,10 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); - runtime.push (MWBase::Environment::get().getMechanicsManager()->getDerivedDisposition(ptr)); + if (!ptr.getClass().isNpc()) + runtime.push(0); + else + runtime.push (MWBase::Environment::get().getMechanicsManager()->getDerivedDisposition(ptr)); } }; @@ -740,6 +766,8 @@ namespace MWScript virtual void execute (Interpreter::Runtime& runtime, unsigned int arg0) { + MWWorld::Ptr ptr = R()(runtime, false); + std::string factionId; if (arg0==1) @@ -749,8 +777,6 @@ namespace MWScript } else { - MWWorld::Ptr ptr = R()(runtime); - if (!ptr.getClass().getNpcStats (ptr).getFactionRanks().empty()) factionId = ptr.getClass().getNpcStats (ptr).getFactionRanks().begin()->first; } @@ -773,6 +799,8 @@ namespace MWScript virtual void execute (Interpreter::Runtime& runtime, unsigned int arg0) { + MWWorld::Ptr ptr = R()(runtime, false); + Interpreter::Type_Integer value = runtime[0].mInteger; runtime.pop(); @@ -785,8 +813,6 @@ namespace MWScript } else { - MWWorld::Ptr ptr = R()(runtime); - if (!ptr.getClass().getNpcStats (ptr).getFactionRanks().empty()) factionId = ptr.getClass().getNpcStats (ptr).getFactionRanks().begin()->first; } @@ -808,6 +834,8 @@ namespace MWScript virtual void execute (Interpreter::Runtime& runtime, unsigned int arg0) { + MWWorld::Ptr ptr = R()(runtime, false); + Interpreter::Type_Integer value = runtime[0].mInteger; runtime.pop(); @@ -820,8 +848,6 @@ namespace MWScript } else { - MWWorld::Ptr ptr = R()(runtime); - if (!ptr.getClass().getNpcStats (ptr).getFactionRanks().empty()) factionId = ptr.getClass().getNpcStats (ptr).getFactionRanks().begin()->first; } @@ -903,7 +929,7 @@ namespace MWScript virtual void execute (Interpreter::Runtime& runtime, unsigned int arg0) { - MWWorld::Ptr ptr = R()(runtime); + MWWorld::Ptr ptr = R()(runtime, false); std::string factionID = ""; if(arg0 >0 ) @@ -942,6 +968,8 @@ namespace MWScript virtual void execute (Interpreter::Runtime& runtime, unsigned int arg0) { + MWWorld::Ptr ptr = R()(runtime, false); + std::string factionID = ""; if(arg0 >0 ) { @@ -950,7 +978,6 @@ namespace MWScript } else { - MWWorld::Ptr ptr = R()(runtime); if(ptr.getClass().getNpcStats(ptr).getFactionRanks().empty()) { factionID = ""; @@ -975,6 +1002,8 @@ namespace MWScript virtual void execute (Interpreter::Runtime& runtime, unsigned int arg0) { + MWWorld::Ptr ptr = R()(runtime, false); + std::string factionID = ""; if(arg0 >0 ) { @@ -983,7 +1012,6 @@ namespace MWScript } else { - MWWorld::Ptr ptr = R()(runtime); if(ptr.getClass().getNpcStats(ptr).getFactionRanks().empty()) { factionID = ""; @@ -1021,8 +1049,7 @@ namespace MWScript if (ptr == player) return; - std::map& ranks = ptr.getClass().getNpcStats(ptr).getFactionRanks (); - ranks[factionID] = ranks[factionID]+1; + ptr.getClass().getNpcStats(ptr).raiseRank(factionID); } }; @@ -1048,8 +1075,7 @@ namespace MWScript if (ptr == player) return; - std::map& ranks = ptr.getClass().getNpcStats(ptr).getFactionRanks (); - ranks[factionID] = ranks[factionID]-1; + ptr.getClass().getNpcStats(ptr).lowerRank(factionID); } }; @@ -1072,6 +1098,25 @@ namespace MWScript } }; + template + class OpOnMurder : public Interpreter::Opcode0 + { + public: + + virtual void execute (Interpreter::Runtime& runtime) + { + MWWorld::Ptr ptr = R()(runtime); + + Interpreter::Type_Integer value = + ptr.getClass().getCreatureStats (ptr).hasBeenMurdered(); + + if (value) + ptr.getClass().getCreatureStats (ptr).clearHasBeenMurdered(); + + runtime.push (value); + } + }; + template class OpOnKnockout : public Interpreter::Opcode0 { @@ -1136,6 +1181,102 @@ namespace MWScript } }; + template + class OpGetStat : public Interpreter::Opcode0 + { + public: + virtual void execute (Interpreter::Runtime& runtime) + { + // dummy + runtime.push(0); + } + }; + + template + class OpGetMagicEffect : public Interpreter::Opcode0 + { + int mPositiveEffect; + int mNegativeEffect; + + public: + OpGetMagicEffect (int positiveEffect, int negativeEffect) + : mPositiveEffect(positiveEffect) + , mNegativeEffect(negativeEffect) + { + } + + virtual void execute (Interpreter::Runtime& runtime) + { + MWWorld::Ptr ptr = R()(runtime); + + MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); + float currentValue = stats.getMagicEffects().get(mPositiveEffect).getMagnitude(); + if (mNegativeEffect != -1) + currentValue -= stats.getMagicEffects().get(mNegativeEffect).getMagnitude(); + + int ret = static_cast(currentValue); + runtime.push(ret); + } + }; + + template + class OpSetMagicEffect : public Interpreter::Opcode0 + { + int mPositiveEffect; + int mNegativeEffect; + + public: + OpSetMagicEffect (int positiveEffect, int negativeEffect) + : mPositiveEffect(positiveEffect) + , mNegativeEffect(negativeEffect) + { + } + + virtual void execute(Interpreter::Runtime &runtime) + { + MWWorld::Ptr ptr = R()(runtime); + MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); + float currentValue = stats.getMagicEffects().get(mPositiveEffect).getMagnitude(); + if (mNegativeEffect != -1) + currentValue -= stats.getMagicEffects().get(mNegativeEffect).getMagnitude(); + currentValue = int(currentValue); + + int arg = runtime[0].mInteger; + runtime.pop(); + stats.getMagicEffects().modifyBase(mPositiveEffect, (arg - currentValue)); + } + }; + + template + class OpModMagicEffect : public Interpreter::Opcode0 + { + int mPositiveEffect; + int mNegativeEffect; + + public: + OpModMagicEffect (int positiveEffect, int negativeEffect) + : mPositiveEffect(positiveEffect) + , mNegativeEffect(negativeEffect) + { + } + + virtual void execute(Interpreter::Runtime &runtime) + { + MWWorld::Ptr ptr = R()(runtime); + MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); + + int arg = runtime[0].mInteger; + runtime.pop(); + stats.getMagicEffects().modifyBase(mPositiveEffect, arg); + } + }; + + struct MagicEffect + { + int mPositiveEffect; + int mNegativeEffect; + }; + void installOpcodes (Interpreter::Interpreter& interpreter) { for (int i=0; i); interpreter.installSegment5 (Compiler::Stats::opcodeOnDeathExplicit, new OpOnDeath); + interpreter.installSegment5 (Compiler::Stats::opcodeOnMurder, new OpOnMurder); + interpreter.installSegment5 (Compiler::Stats::opcodeOnMurderExplicit, new OpOnMurder); interpreter.installSegment5 (Compiler::Stats::opcodeOnKnockout, new OpOnKnockout); interpreter.installSegment5 (Compiler::Stats::opcodeOnKnockoutExplicit, new OpOnKnockout); @@ -1276,6 +1419,50 @@ namespace MWScript interpreter.installSegment5 (Compiler::Stats::opcodeUndoWerewolfExplicit, new OpSetWerewolf); interpreter.installSegment5 (Compiler::Stats::opcodeSetWerewolfAcrobatics, new OpSetWerewolfAcrobatics); interpreter.installSegment5 (Compiler::Stats::opcodeSetWerewolfAcrobaticsExplicit, new OpSetWerewolfAcrobatics); + interpreter.installSegment5 (Compiler::Stats::opcodeGetStat, new OpGetStat); + interpreter.installSegment5 (Compiler::Stats::opcodeGetStatExplicit, new OpGetStat); + + static const MagicEffect sMagicEffects[] = { + { ESM::MagicEffect::ResistMagicka, ESM::MagicEffect::WeaknessToMagicka }, + { ESM::MagicEffect::ResistFire, ESM::MagicEffect::WeaknessToFire }, + { ESM::MagicEffect::ResistFrost, ESM::MagicEffect::WeaknessToFrost }, + { ESM::MagicEffect::ResistShock, ESM::MagicEffect::WeaknessToShock }, + { ESM::MagicEffect::ResistCommonDisease, ESM::MagicEffect::WeaknessToCommonDisease }, + { ESM::MagicEffect::ResistBlightDisease, ESM::MagicEffect::WeaknessToBlightDisease }, + { ESM::MagicEffect::ResistCorprusDisease, ESM::MagicEffect::WeaknessToCorprusDisease }, + { ESM::MagicEffect::ResistPoison, ESM::MagicEffect::WeaknessToPoison }, + { ESM::MagicEffect::ResistParalysis, -1 }, + { ESM::MagicEffect::ResistNormalWeapons, ESM::MagicEffect::WeaknessToNormalWeapons }, + { ESM::MagicEffect::WaterBreathing, -1 }, + { ESM::MagicEffect::Chameleon, -1 }, + { ESM::MagicEffect::WaterWalking, -1 }, + { ESM::MagicEffect::SwiftSwim, -1 }, + { ESM::MagicEffect::Jump, -1 }, + { ESM::MagicEffect::Levitate, -1 }, + { ESM::MagicEffect::Shield, -1 }, + { ESM::MagicEffect::Sound, -1 }, + { ESM::MagicEffect::Silence, -1 }, + { ESM::MagicEffect::Blind, -1 }, + { ESM::MagicEffect::Paralyze, -1 }, + { ESM::MagicEffect::Invisibility, -1 }, + { ESM::MagicEffect::FortifyAttack, -1 }, + { ESM::MagicEffect::Sanctuary, -1 }, + }; + + for (int i=0; i<24; ++i) + { + int positive = sMagicEffects[i].mPositiveEffect; + int negative = sMagicEffects[i].mNegativeEffect; + + interpreter.installSegment5 (Compiler::Stats::opcodeGetMagicEffect+i, new OpGetMagicEffect (positive, negative)); + interpreter.installSegment5 (Compiler::Stats::opcodeGetMagicEffectExplicit+i, new OpGetMagicEffect (positive, negative)); + + interpreter.installSegment5 (Compiler::Stats::opcodeSetMagicEffect+i, new OpSetMagicEffect (positive, negative)); + interpreter.installSegment5 (Compiler::Stats::opcodeSetMagicEffectExplicit+i, new OpSetMagicEffect (positive, negative)); + + interpreter.installSegment5 (Compiler::Stats::opcodeModMagicEffect+i, new OpModMagicEffect (positive, negative)); + interpreter.installSegment5 (Compiler::Stats::opcodeModMagicEffectExplicit+i, new OpModMagicEffect (positive, negative)); + } } } } diff --git a/apps/openmw/mwscript/transformationextensions.cpp b/apps/openmw/mwscript/transformationextensions.cpp index a944a31b8..e74388eff 100644 --- a/apps/openmw/mwscript/transformationextensions.cpp +++ b/apps/openmw/mwscript/transformationextensions.cpp @@ -86,16 +86,24 @@ namespace MWScript float ay = Ogre::Radian(ptr.getRefData().getPosition().rot[1]).valueDegrees(); float az = Ogre::Radian(ptr.getRefData().getPosition().rot[2]).valueDegrees(); + MWWorld::LocalRotation localRot = ptr.getRefData().getLocalRotation(); + if (axis == "x") { + localRot.rot[0] = 0; + ptr.getRefData().setLocalRotation(localRot); MWBase::Environment::get().getWorld()->rotateObject(ptr,angle,ay,az); } else if (axis == "y") { + localRot.rot[1] = 0; + ptr.getRefData().setLocalRotation(localRot); MWBase::Environment::get().getWorld()->rotateObject(ptr,ax,angle,az); } else if (axis == "z") { + localRot.rot[2] = 0; + ptr.getRefData().setLocalRotation(localRot); MWBase::Environment::get().getWorld()->rotateObject(ptr,ax,ay,angle); } else @@ -319,11 +327,11 @@ namespace MWScript } MWBase::Environment::get().getWorld()->rotateObject(ptr,ax,ay,zRot); - ptr.getClass().adjustPosition(ptr); + ptr.getClass().adjustPosition(ptr, false); } else { - throw std::runtime_error ("unknown cell"); + throw std::runtime_error (std::string("unknown cell (") + cellID + ")"); } } }; @@ -355,8 +363,19 @@ namespace MWScript runtime.pop(); int cx,cy; MWBase::Environment::get().getWorld()->positionToIndex(x,y,cx,cy); - MWBase::Environment::get().getWorld()->moveObject(ptr, - MWBase::Environment::get().getWorld()->getExterior(cx,cy),x,y,z); + + // another morrowind oddity: player will be moved to the exterior cell at this location, + // non-player actors will move within the cell they are in. + if (ptr.getRefData().getHandle() == "player") + { + MWBase::Environment::get().getWorld()->moveObject(ptr, + MWBase::Environment::get().getWorld()->getExterior(cx,cy),x,y,z); + } + else + { + MWBase::Environment::get().getWorld()->moveObject(ptr, x, y, z); + } + float ax = Ogre::Radian(ptr.getRefData().getPosition().rot[0]).valueDegrees(); float ay = Ogre::Radian(ptr.getRefData().getPosition().rot[1]).valueDegrees(); if(ptr.getTypeName() == typeid(ESM::NPC).name())//some morrowind oddity @@ -366,7 +385,7 @@ namespace MWScript zRot = zRot/60.; } MWBase::Environment::get().getWorld()->rotateObject(ptr,ax,ay,zRot); - ptr.getClass().adjustPosition(ptr); + ptr.getClass().adjustPosition(ptr, false); } }; @@ -420,7 +439,7 @@ namespace MWScript } else { - throw std::runtime_error ("unknown cell"); + throw std::runtime_error ( std::string("unknown cell (") + cellID + ")"); } } }; @@ -444,25 +463,26 @@ namespace MWScript Interpreter::Type_Float zRot = runtime[0].mFloat; runtime.pop(); - int cx,cy; - MWBase::Environment::get().getWorld()->positionToIndex(x,y,cx,cy); - MWWorld::CellStore* store = MWBase::Environment::get().getWorld()->getExterior(cx,cy); - if(store) + MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); + MWWorld::CellStore* store = NULL; + if (player.getCell()->isExterior()) { - ESM::Position pos; - pos.pos[0] = x; - pos.pos[1] = y; - pos.pos[2] = z; - pos.rot[0] = pos.rot[1] = 0; - pos.rot[2] = zRot; - MWWorld::ManualRef ref(MWBase::Environment::get().getWorld()->getStore(),itemID); - ref.getPtr().getCellRef().setPosition(pos); - MWBase::Environment::get().getWorld()->safePlaceObject(ref.getPtr(),store,pos); + int cx,cy; + MWBase::Environment::get().getWorld()->positionToIndex(x,y,cx,cy); + store = MWBase::Environment::get().getWorld()->getExterior(cx,cy); } else - { - throw std::runtime_error ("unknown cell"); - } + store = player.getCell(); + + ESM::Position pos; + pos.pos[0] = x; + pos.pos[1] = y; + pos.pos[2] = z; + pos.rot[0] = pos.rot[1] = 0; + pos.rot[2] = zRot; + MWWorld::ManualRef ref(MWBase::Environment::get().getWorld()->getStore(),itemID); + ref.getPtr().getCellRef().setPosition(pos); + MWBase::Environment::get().getWorld()->safePlaceObject(ref.getPtr(),store,pos); } }; @@ -578,7 +598,7 @@ namespace MWScript Interpreter::Type_Float rotation = (runtime[0].mFloat*MWBase::Environment::get().getFrameDuration()); runtime.pop(); - float *objRot = ptr.getRefData().getPosition().rot; + const float *objRot = ptr.getRefData().getPosition().rot; float ax = Ogre::Radian(objRot[0]).valueDegrees(); float ay = Ogre::Radian(objRot[1]).valueDegrees(); @@ -613,9 +633,12 @@ namespace MWScript if (!ptr.isInCell()) return; - ptr.getRefData().getLocalRotation().rot[0] = 0; - ptr.getRefData().getLocalRotation().rot[1] = 0; - ptr.getRefData().getLocalRotation().rot[2] = 0; + MWWorld::LocalRotation rot; + rot.rot[0] = 0; + rot.rot[1] = 0; + rot.rot[2] = 0; + ptr.getRefData().setLocalRotation(rot); + MWBase::Environment::get().getWorld()->rotateObject(ptr, 0,0,0,true); MWBase::Environment::get().getWorld()->moveObject(ptr, ptr.getCellRef().getPosition().pos[0], ptr.getCellRef().getPosition().pos[1], ptr.getCellRef().getPosition().pos[2]); @@ -678,7 +701,7 @@ namespace MWScript Interpreter::Type_Float movement = (runtime[0].mFloat*MWBase::Environment::get().getFrameDuration()); runtime.pop(); - float *objPos = ptr.getRefData().getPosition().pos; + const float *objPos = ptr.getRefData().getPosition().pos; if (axis == "x") { @@ -697,6 +720,15 @@ namespace MWScript } }; + class OpResetActors : public Interpreter::Opcode0 + { + public: + + virtual void execute (Interpreter::Runtime& runtime) + { + MWBase::Environment::get().getWorld()->resetActors(); + } + }; void installOpcodes (Interpreter::Interpreter& interpreter) { @@ -737,6 +769,7 @@ namespace MWScript interpreter.installSegment5(Compiler::Transformation::opcodeMoveWorldExplicit,new OpMoveWorld); interpreter.installSegment5(Compiler::Transformation::opcodeGetStartingAngle, new OpGetStartingAngle); interpreter.installSegment5(Compiler::Transformation::opcodeGetStartingAngleExplicit, new OpGetStartingAngle); + interpreter.installSegment5(Compiler::Transformation::opcodeResetActors, new OpResetActors); } } } diff --git a/apps/openmw/mwsound/ffmpeg_decoder.cpp b/apps/openmw/mwsound/ffmpeg_decoder.cpp index 75f7ccae4..2eddc98d3 100644 --- a/apps/openmw/mwsound/ffmpeg_decoder.cpp +++ b/apps/openmw/mwsound/ffmpeg_decoder.cpp @@ -1,6 +1,3 @@ -#ifdef OPENMW_USE_FFMPEG - - #include "ffmpeg_decoder.hpp" // auto_ptr @@ -8,6 +5,16 @@ #include +extern "C" { +#ifndef HAVE_LIBSWRESAMPLE +// FIXME: remove this section once libswresample is packaged for Debian +int swr_init(AVAudioResampleContext *avr); +void swr_free(AVAudioResampleContext **avr); +int swr_convert( AVAudioResampleContext *avr, uint8_t** output, int out_samples, const uint8_t** input, int in_samples); +AVAudioResampleContext * swr_alloc_set_opts( AVAudioResampleContext *avr, int64_t out_ch_layout, AVSampleFormat out_fmt, int out_rate, int64_t in_ch_layout, AVSampleFormat in_fmt, int in_rate, int o, void* l); +#endif +} + namespace MWSound { @@ -62,7 +69,7 @@ bool FFmpeg_Decoder::getNextPacket() /* Check if the packet belongs to this stream */ if(stream_idx == mPacket.stream_index) { - if((uint64_t)mPacket.pts != AV_NOPTS_VALUE) + if(mPacket.pts != (int64_t)AV_NOPTS_VALUE) mNextPts = av_q2d((*mStream)->time_base)*mPacket.pts; return true; } @@ -76,7 +83,7 @@ bool FFmpeg_Decoder::getNextPacket() bool FFmpeg_Decoder::getAVAudioData() { - int got_frame, len; + int got_frame; if((*mStream)->codec->codec_type != AVMEDIA_TYPE_AUDIO) return false; @@ -86,6 +93,7 @@ bool FFmpeg_Decoder::getAVAudioData() return false; /* Decode some data, and check for errors */ + int len = 0; if((len=avcodec_decode_audio4((*mStream)->codec, mFrame, &got_frame, &mPacket)) < 0) return false; @@ -98,6 +106,32 @@ bool FFmpeg_Decoder::getAVAudioData() memmove(mPacket.data, &mPacket.data[len], remaining); av_shrink_packet(&mPacket, remaining); } + + if (!got_frame || mFrame->nb_samples == 0) + continue; + + if(mSwr) + { + if(!mDataBuf || mDataBufLen < mFrame->nb_samples) + { + av_freep(&mDataBuf); + if(av_samples_alloc(&mDataBuf, NULL, av_get_channel_layout_nb_channels(mOutputChannelLayout), + mFrame->nb_samples, mOutputSampleFormat, 0) < 0) + return false; + else + mDataBufLen = mFrame->nb_samples; + } + + if(swr_convert(mSwr, (uint8_t**)&mDataBuf, mFrame->nb_samples, + (const uint8_t**)mFrame->extended_data, mFrame->nb_samples) < 0) + { + return false; + } + mFrameData = &mDataBuf; + } + else + mFrameData = &mFrame->data[0]; + } while(got_frame == 0 || mFrame->nb_samples == 0); mNextPts += (double)mFrame->nb_samples / (double)(*mStream)->codec->sample_rate; @@ -116,8 +150,8 @@ size_t FFmpeg_Decoder::readAVAudioData(void *data, size_t length) if(!getAVAudioData()) break; mFramePos = 0; - mFrameSize = mFrame->nb_samples * (*mStream)->codec->channels * - av_get_bytes_per_sample((*mStream)->codec->sample_fmt); + mFrameSize = mFrame->nb_samples * av_get_channel_layout_nb_channels(mOutputChannelLayout) * + av_get_bytes_per_sample(mOutputSampleFormat); } /* Get the amount of bytes remaining to be written, and clamp to @@ -125,7 +159,7 @@ size_t FFmpeg_Decoder::readAVAudioData(void *data, size_t length) size_t rem = std::min(length-dec, mFrameSize-mFramePos); /* Copy the data to the app's buffer and increment */ - memcpy(data, mFrame->data[0]+mFramePos, rem); + memcpy(data, mFrameData[0]+mFramePos, rem); data = (char*)data + rem; dec += rem; mFramePos += rem; @@ -135,19 +169,6 @@ size_t FFmpeg_Decoder::readAVAudioData(void *data, size_t length) return dec; } -static AVSampleFormat ffmpegNonPlanarSampleFormat (AVSampleFormat format) -{ - switch (format) - { - case AV_SAMPLE_FMT_U8P: return AV_SAMPLE_FMT_U8; - case AV_SAMPLE_FMT_S16P: return AV_SAMPLE_FMT_S16; - case AV_SAMPLE_FMT_S32P: return AV_SAMPLE_FMT_S32; - case AV_SAMPLE_FMT_FLTP: return AV_SAMPLE_FMT_FLT; - case AV_SAMPLE_FMT_DBLP: return AV_SAMPLE_FMT_DBL; - default:return format; - } -} - void FFmpeg_Decoder::open(const std::string &fname) { close(); @@ -159,17 +180,21 @@ void FFmpeg_Decoder::open(const std::string &fname) mFormatCtx->pb = avio_alloc_context(NULL, 0, 0, this, readPacket, writePacket, seek); if(!mFormatCtx->pb || avformat_open_input(&mFormatCtx, fname.c_str(), NULL, NULL) != 0) { - if (mFormatCtx->pb != NULL) + // "Note that a user-supplied AVFormatContext will be freed on failure". + if (mFormatCtx) { - if (mFormatCtx->pb->buffer != NULL) - { - av_free(mFormatCtx->pb->buffer); - mFormatCtx->pb->buffer = NULL; - } - av_free(mFormatCtx->pb); - mFormatCtx->pb = NULL; + if (mFormatCtx->pb != NULL) + { + if (mFormatCtx->pb->buffer != NULL) + { + av_free(mFormatCtx->pb->buffer); + mFormatCtx->pb->buffer = NULL; + } + av_free(mFormatCtx->pb); + mFormatCtx->pb = NULL; + } + avformat_free_context(mFormatCtx); } - avformat_free_context(mFormatCtx); mFormatCtx = NULL; fail("Failed to allocate input stream"); } @@ -190,7 +215,7 @@ void FFmpeg_Decoder::open(const std::string &fname) if(!mStream) fail("No audio streams in "+fname); - (*mStream)->codec->request_sample_fmt = ffmpegNonPlanarSampleFormat ((*mStream)->codec->sample_fmt); + (*mStream)->codec->request_sample_fmt = (*mStream)->codec->sample_fmt; AVCodec *codec = avcodec_find_decoder((*mStream)->codec->codec_id); if(!codec) @@ -202,7 +227,7 @@ void FFmpeg_Decoder::open(const std::string &fname) if(avcodec_open2((*mStream)->codec, codec, NULL) < 0) fail("Failed to open audio codec " + std::string(codec->long_name)); - mFrame = avcodec_alloc_frame(); + mFrame = av_frame_alloc(); } catch(std::exception&) { @@ -227,6 +252,8 @@ void FFmpeg_Decoder::close() av_free_packet(&mPacket); av_freep(&mFrame); + swr_free(&mSwr); + av_freep(&mDataBuf); if(mFormatCtx) { @@ -261,40 +288,45 @@ void FFmpeg_Decoder::getInfo(int *samplerate, ChannelConfig *chans, SampleType * if(!mStream) fail("No audio stream info"); - if((*mStream)->codec->sample_fmt == AV_SAMPLE_FMT_U8) + if((*mStream)->codec->sample_fmt == AV_SAMPLE_FMT_FLT || (*mStream)->codec->sample_fmt == AV_SAMPLE_FMT_FLTP) + mOutputSampleFormat = AV_SAMPLE_FMT_S16; // FIXME: Check for AL_EXT_FLOAT32 support + else if((*mStream)->codec->sample_fmt == AV_SAMPLE_FMT_U8P) + mOutputSampleFormat = AV_SAMPLE_FMT_U8; + else if((*mStream)->codec->sample_fmt == AV_SAMPLE_FMT_S16P) + mOutputSampleFormat = AV_SAMPLE_FMT_S16; + else + mOutputSampleFormat = AV_SAMPLE_FMT_S16; + + if(mOutputSampleFormat == AV_SAMPLE_FMT_U8) *type = SampleType_UInt8; - else if((*mStream)->codec->sample_fmt == AV_SAMPLE_FMT_S16) + else if(mOutputSampleFormat == AV_SAMPLE_FMT_S16) *type = SampleType_Int16; - else if((*mStream)->codec->sample_fmt == AV_SAMPLE_FMT_FLT) + else if(mOutputSampleFormat == AV_SAMPLE_FMT_FLT) *type = SampleType_Float32; - else - fail(std::string("Unsupported sample format: ")+ - av_get_sample_fmt_name((*mStream)->codec->sample_fmt)); - if((*mStream)->codec->channel_layout == AV_CH_LAYOUT_MONO) + int64_t ch_layout = (*mStream)->codec->channel_layout; + + if(ch_layout == 0) + ch_layout = av_get_default_channel_layout((*mStream)->codec->channels); + + mOutputChannelLayout = ch_layout; + if (ch_layout == AV_CH_LAYOUT_5POINT1 || ch_layout == AV_CH_LAYOUT_7POINT1 + || ch_layout == AV_CH_LAYOUT_QUAD) // FIXME: check for AL_EXT_MCFORMATS support + mOutputChannelLayout = AV_CH_LAYOUT_STEREO; + else if (ch_layout != AV_CH_LAYOUT_MONO + && ch_layout != AV_CH_LAYOUT_STEREO) + mOutputChannelLayout = AV_CH_LAYOUT_STEREO; + + if(mOutputChannelLayout == AV_CH_LAYOUT_MONO) *chans = ChannelConfig_Mono; - else if((*mStream)->codec->channel_layout == AV_CH_LAYOUT_STEREO) + else if(mOutputChannelLayout == AV_CH_LAYOUT_STEREO) *chans = ChannelConfig_Stereo; - else if((*mStream)->codec->channel_layout == AV_CH_LAYOUT_QUAD) + else if(mOutputChannelLayout == AV_CH_LAYOUT_QUAD) *chans = ChannelConfig_Quad; - else if((*mStream)->codec->channel_layout == AV_CH_LAYOUT_5POINT1) + else if(mOutputChannelLayout == AV_CH_LAYOUT_5POINT1) *chans = ChannelConfig_5point1; - else if((*mStream)->codec->channel_layout == AV_CH_LAYOUT_7POINT1) + else if(mOutputChannelLayout == AV_CH_LAYOUT_7POINT1) *chans = ChannelConfig_7point1; - else if((*mStream)->codec->channel_layout == 0) - { - /* Unknown channel layout. Try to guess. */ - if((*mStream)->codec->channels == 1) - *chans = ChannelConfig_Mono; - else if((*mStream)->codec->channels == 2) - *chans = ChannelConfig_Stereo; - else - { - std::stringstream sstr("Unsupported raw channel count: "); - sstr << (*mStream)->codec->channels; - fail(sstr.str()); - } - } else { char str[1024]; @@ -304,6 +336,25 @@ void FFmpeg_Decoder::getInfo(int *samplerate, ChannelConfig *chans, SampleType * } *samplerate = (*mStream)->codec->sample_rate; + + if(mOutputSampleFormat != (*mStream)->codec->sample_fmt + || mOutputChannelLayout != ch_layout) + { + mSwr = swr_alloc_set_opts(mSwr, // SwrContext + mOutputChannelLayout, // output ch layout + mOutputSampleFormat, // output sample format + (*mStream)->codec->sample_rate, // output sample rate + ch_layout, // input ch layout + (*mStream)->codec->sample_fmt, // input sample format + (*mStream)->codec->sample_rate, // input sample rate + 0, // logging level offset + NULL); // log context + if(!mSwr) + fail(std::string("Couldn't allocate SwrContext")); + if(swr_init(mSwr) < 0) + fail(std::string("Couldn't initialize SwrContext")); + + } } size_t FFmpeg_Decoder::read(char *buffer, size_t bytes) @@ -320,9 +371,9 @@ void FFmpeg_Decoder::readAll(std::vector &output) while(getAVAudioData()) { - size_t got = mFrame->nb_samples * (*mStream)->codec->channels * - av_get_bytes_per_sample((*mStream)->codec->sample_fmt); - const char *inbuf = reinterpret_cast(mFrame->data[0]); + size_t got = mFrame->nb_samples * av_get_channel_layout_nb_channels(mOutputChannelLayout) * + av_get_bytes_per_sample(mOutputSampleFormat); + const char *inbuf = reinterpret_cast(mFrameData[0]); output.insert(output.end(), inbuf, inbuf+got); } } @@ -339,8 +390,8 @@ void FFmpeg_Decoder::rewind() size_t FFmpeg_Decoder::getSampleOffset() { - int delay = (mFrameSize-mFramePos) / (*mStream)->codec->channels / - av_get_bytes_per_sample((*mStream)->codec->sample_fmt); + int delay = (mFrameSize-mFramePos) / av_get_channel_layout_nb_channels(mOutputChannelLayout) / + av_get_bytes_per_sample(mOutputSampleFormat); return (int)(mNextPts*(*mStream)->codec->sample_rate) - delay; } @@ -351,6 +402,12 @@ FFmpeg_Decoder::FFmpeg_Decoder() , mFrameSize(0) , mFramePos(0) , mNextPts(0.0) + , mSwr(0) + , mOutputSampleFormat(AV_SAMPLE_FMT_NONE) + , mOutputChannelLayout(0) + , mDataBuf(NULL) + , mFrameData(NULL) + , mDataBufLen(0) { memset(&mPacket, 0, sizeof(mPacket)); @@ -371,5 +428,3 @@ FFmpeg_Decoder::~FFmpeg_Decoder() } } - -#endif diff --git a/apps/openmw/mwsound/ffmpeg_decoder.hpp b/apps/openmw/mwsound/ffmpeg_decoder.hpp index 8276b45c7..2cdbbf363 100644 --- a/apps/openmw/mwsound/ffmpeg_decoder.hpp +++ b/apps/openmw/mwsound/ffmpeg_decoder.hpp @@ -4,7 +4,9 @@ // FIXME: This can't be right? The headers refuse to build without UINT64_C, // which only gets defined in stdint.h in either C99 mode or with this macro // defined... +#ifndef __STDC_CONSTANT_MACROS #define __STDC_CONSTANT_MACROS +#endif #include extern "C" { @@ -18,6 +20,21 @@ extern "C" LIBAVUTIL_VERSION_MINOR, LIBAVUTIL_VERSION_MICRO) #include #endif + +#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(55,28,1) +#define av_frame_alloc avcodec_alloc_frame +#endif + +// From version 54.56 binkaudio encoding format changed from S16 to FLTP. See: +// https://gitorious.org/ffmpeg/ffmpeg/commit/7bfd1766d1c18f07b0a2dd042418a874d49ea60d +// http://ffmpeg.zeranoe.com/forum/viewtopic.php?f=15&t=872 +#ifdef HAVE_LIBSWRESAMPLE +#include +#else +#include +#include +#define SwrContext AVAudioResampleContext +#endif } #include @@ -40,6 +57,13 @@ namespace MWSound double mNextPts; + SwrContext *mSwr; + enum AVSampleFormat mOutputSampleFormat; + int64_t mOutputChannelLayout; + uint8_t *mDataBuf; + uint8_t **mFrameData; + int mDataBufLen; + bool getNextPacket(); Ogre::DataStreamPtr mDataStream; diff --git a/apps/openmw/mwsound/libavwrapper.cpp b/apps/openmw/mwsound/libavwrapper.cpp new file mode 100644 index 000000000..a7a3245da --- /dev/null +++ b/apps/openmw/mwsound/libavwrapper.cpp @@ -0,0 +1,111 @@ +#ifndef HAVE_LIBSWRESAMPLE +extern "C" +{ +#ifndef __STDC_CONSTANT_MACROS +#define __STDC_CONSTANT_MACROS +#endif +#include + +#include +#include +// From libavutil version 52.2.0 and onward the declaration of +// AV_CH_LAYOUT_* is removed from libavcodec/avcodec.h and moved to +// libavutil/channel_layout.h +#if AV_VERSION_INT(52, 2, 0) <= AV_VERSION_INT(LIBAVUTIL_VERSION_MAJOR, \ + LIBAVUTIL_VERSION_MINOR, LIBAVUTIL_VERSION_MICRO) + #include +#endif +#include +#include + +/* FIXME: delete this file once libswresample is packaged for Debian */ + +int swr_init(AVAudioResampleContext *avr) { return 1; } + +void swr_free(AVAudioResampleContext **avr) { avresample_free(avr); } + +int swr_convert( + AVAudioResampleContext *avr, + uint8_t** output, + int out_samples, + const uint8_t** input, + int in_samples) +{ + // FIXME: potential performance hit + int out_plane_size = 0; + int in_plane_size = 0; + return avresample_convert(avr, output, out_plane_size, out_samples, + (uint8_t **)input, in_plane_size, in_samples); +} + +AVAudioResampleContext * swr_alloc_set_opts( + AVAudioResampleContext *avr, + int64_t out_ch_layout, + AVSampleFormat out_fmt, + int out_rate, + int64_t in_ch_layout, + AVSampleFormat in_fmt, + int in_rate, + int o, + void* l) +{ + avr = avresample_alloc_context(); + if(!avr) + return 0; + + int res; + res = av_opt_set_int(avr, "out_channel_layout", out_ch_layout, 0); + if(res < 0) + { + av_log(avr, AV_LOG_ERROR, "av_opt_set_int: out_ch_layout = %d\n", res); + return 0; + } + res = av_opt_set_int(avr, "out_sample_fmt", out_fmt, 0); + if(res < 0) + { + av_log(avr, AV_LOG_ERROR, "av_opt_set_int: out_fmt = %d\n", res); + return 0; + } + res = av_opt_set_int(avr, "out_sample_rate", out_rate, 0); + if(res < 0) + { + av_log(avr, AV_LOG_ERROR, "av_opt_set_int: out_rate = %d\n", res); + return 0; + } + res = av_opt_set_int(avr, "in_channel_layout", in_ch_layout, 0); + if(res < 0) + { + av_log(avr, AV_LOG_ERROR, "av_opt_set_int: in_ch_layout = %d\n", res); + return 0; + } + res = av_opt_set_int(avr, "in_sample_fmt", in_fmt, 0); + if(res < 0) + { + av_log(avr, AV_LOG_ERROR, "av_opt_set_int: in_fmt = %d\n", res); + return 0; + } + res = av_opt_set_int(avr, "in_sample_rate", in_rate, 0); + if(res < 0) + { + av_log(avr, AV_LOG_ERROR, "av_opt_set_int: in_rate = %d\n", res); + return 0; + } + res = av_opt_set_int(avr, "internal_sample_fmt", AV_SAMPLE_FMT_FLTP, 0); + if(res < 0) + { + av_log(avr, AV_LOG_ERROR, "av_opt_set_int: internal_sample_fmt\n"); + return 0; + } + + + if(avresample_open(avr) < 0) + { + av_log(avr, AV_LOG_ERROR, "Error opening context\n"); + return 0; + } + else + return avr; +} + +} +#endif diff --git a/apps/openmw/mwsound/loudness.cpp b/apps/openmw/mwsound/loudness.cpp new file mode 100644 index 000000000..c64913b1f --- /dev/null +++ b/apps/openmw/mwsound/loudness.cpp @@ -0,0 +1,53 @@ +#include "loudness.hpp" + +#include "soundmanagerimp.hpp" + +namespace MWSound +{ + + void analyzeLoudness(const std::vector &data, int sampleRate, ChannelConfig chans, + SampleType type, std::vector &out, float valuesPerSecond) + { + int samplesPerSegment = sampleRate / valuesPerSecond; + int numSamples = bytesToFrames(data.size(), chans, type); + int advance = framesToBytes(1, chans, type); + + out.reserve(numSamples/samplesPerSegment); + + int segment=0; + int sample=0; + while (segment < numSamples/samplesPerSegment) + { + float sum=0; + int samplesAdded = 0; + while (sample < numSamples && sample < (segment+1)*samplesPerSegment) + { + // get sample on a scale from -1 to 1 + float value = 0; + if (type == SampleType_UInt8) + value = ((char)(data[sample*advance]^0x80))/128.f; + else if (type == SampleType_Int16) + { + value = *reinterpret_cast(&data[sample*advance]); + value /= float(std::numeric_limits::max()); + } + else if (type == SampleType_Float32) + { + value = *reinterpret_cast(&data[sample*advance]); + value = std::max(-1.f, std::min(1.f, value)); // Float samples *should* be scaled to [-1,1] already. + } + + sum += value*value; + ++samplesAdded; + ++sample; + } + + float rms = 0; // root mean square + if (samplesAdded > 0) + rms = std::sqrt(sum / samplesAdded); + out.push_back(rms); + ++segment; + } + } + +} diff --git a/apps/openmw/mwsound/loudness.hpp b/apps/openmw/mwsound/loudness.hpp new file mode 100644 index 000000000..df727bd0b --- /dev/null +++ b/apps/openmw/mwsound/loudness.hpp @@ -0,0 +1,20 @@ +#include "sound_decoder.hpp" + +namespace MWSound +{ + +/** + * Analyzes the energy (closely related to loudness) of a sound buffer. + * The buffer will be divided into segments according to \a valuesPerSecond, + * and for each segment a loudness value in the range of [0,1] will be computed. + * @param data the sound buffer to analyze, containing raw samples + * @param sampleRate the sample rate of the sound buffer + * @param chans channel layout of the buffer + * @param type sample type of the buffer + * @param out Will contain the output loudness values. + * @param valuesPerSecond How many loudness values per second of audio to compute. + */ +void analyzeLoudness (const std::vector& data, int sampleRate, ChannelConfig chans, SampleType type, + std::vector& out, float valuesPerSecond); + +} diff --git a/apps/openmw/mwsound/movieaudiofactory.cpp b/apps/openmw/mwsound/movieaudiofactory.cpp new file mode 100644 index 000000000..468f8c82c --- /dev/null +++ b/apps/openmw/mwsound/movieaudiofactory.cpp @@ -0,0 +1,173 @@ +#include "movieaudiofactory.hpp" + +#include +#include + +#include "../mwbase/environment.hpp" +#include "../mwbase/soundmanager.hpp" + +#include "sound_decoder.hpp" +#include "sound.hpp" + +namespace MWSound +{ + + class MovieAudioDecoder; + class MWSoundDecoderBridge : public Sound_Decoder + { + public: + MWSoundDecoderBridge(MWSound::MovieAudioDecoder* decoder) + : mDecoder(decoder) + { + } + + private: + MWSound::MovieAudioDecoder* mDecoder; + + virtual void open(const std::string &fname); + virtual void close(); + virtual void rewind(); + virtual std::string getName(); + virtual void getInfo(int *samplerate, ChannelConfig *chans, SampleType *type); + virtual size_t read(char *buffer, size_t bytes); + virtual size_t getSampleOffset(); + }; + + class MovieAudioDecoder : public Video::MovieAudioDecoder + { + public: + MovieAudioDecoder(Video::VideoState *videoState) + : Video::MovieAudioDecoder(videoState) + { + mDecoderBridge.reset(new MWSoundDecoderBridge(this)); + } + + size_t getSampleOffset() + { + ssize_t clock_delay = (mFrameSize-mFramePos) / av_get_channel_layout_nb_channels(mOutputChannelLayout) / + av_get_bytes_per_sample(mOutputSampleFormat); + return (size_t)(mAudioClock*mAVStream->codec->sample_rate) - clock_delay; + } + + std::string getStreamName() + { + return mVideoState->stream->getName(); + } + + private: + // MovieAudioDecoder overrides + + virtual double getAudioClock() + { + return mAudioTrack->getTimeOffset(); + } + + virtual void adjustAudioSettings(AVSampleFormat& sampleFormat, uint64_t& channelLayout, int& sampleRate) + { + if (sampleFormat == AV_SAMPLE_FMT_U8P || sampleFormat == AV_SAMPLE_FMT_U8) + sampleFormat = AV_SAMPLE_FMT_U8; + else if (sampleFormat == AV_SAMPLE_FMT_S16P || sampleFormat == AV_SAMPLE_FMT_S16) + sampleFormat = AV_SAMPLE_FMT_S16; + else if (sampleFormat == AV_SAMPLE_FMT_FLTP || sampleFormat == AV_SAMPLE_FMT_FLT) + sampleFormat = AV_SAMPLE_FMT_S16; // FIXME: check for AL_EXT_FLOAT32 support + else + sampleFormat = AV_SAMPLE_FMT_S16; + + if (channelLayout == AV_CH_LAYOUT_5POINT1 || channelLayout == AV_CH_LAYOUT_7POINT1 + || channelLayout == AV_CH_LAYOUT_QUAD) // FIXME: check for AL_EXT_MCFORMATS support + channelLayout = AV_CH_LAYOUT_STEREO; + else if (channelLayout != AV_CH_LAYOUT_MONO + && channelLayout != AV_CH_LAYOUT_STEREO) + channelLayout = AV_CH_LAYOUT_STEREO; + } + + public: + ~MovieAudioDecoder() + { + mAudioTrack.reset(); + mDecoderBridge.reset(); + } + + MWBase::SoundPtr mAudioTrack; + boost::shared_ptr mDecoderBridge; + }; + + + void MWSoundDecoderBridge::open(const std::string &fname) + { + throw std::runtime_error("unimplemented"); + } + void MWSoundDecoderBridge::close() {} + void MWSoundDecoderBridge::rewind() {} + + std::string MWSoundDecoderBridge::getName() + { + return mDecoder->getStreamName(); + } + + void MWSoundDecoderBridge::getInfo(int *samplerate, ChannelConfig *chans, SampleType *type) + { + *samplerate = mDecoder->getOutputSampleRate(); + + uint64_t outputChannelLayout = mDecoder->getOutputChannelLayout(); + if (outputChannelLayout == AV_CH_LAYOUT_MONO) + *chans = ChannelConfig_Mono; + else if (outputChannelLayout == AV_CH_LAYOUT_5POINT1) + *chans = ChannelConfig_5point1; + else if (outputChannelLayout == AV_CH_LAYOUT_7POINT1) + *chans = ChannelConfig_7point1; + else if (outputChannelLayout == AV_CH_LAYOUT_STEREO) + *chans = ChannelConfig_Stereo; + else if (outputChannelLayout == AV_CH_LAYOUT_QUAD) + *chans = ChannelConfig_Quad; + else + { + std::stringstream error; + error << "Unsupported channel layout: " << outputChannelLayout; + throw std::runtime_error(error.str()); + } + + AVSampleFormat outputSampleFormat = mDecoder->getOutputSampleFormat(); + if (outputSampleFormat == AV_SAMPLE_FMT_U8) + *type = SampleType_UInt8; + else if (outputSampleFormat == AV_SAMPLE_FMT_FLT) + *type = SampleType_Float32; + else if (outputSampleFormat == AV_SAMPLE_FMT_S16) + *type = SampleType_Int16; + else + { + char str[1024]; + av_get_sample_fmt_string(str, sizeof(str), outputSampleFormat); + throw std::runtime_error(std::string("Unsupported sample format: ") + str); + } + } + + size_t MWSoundDecoderBridge::read(char *buffer, size_t bytes) + { + return mDecoder->read(buffer, bytes); + } + + size_t MWSoundDecoderBridge::getSampleOffset() + { + return mDecoder->getSampleOffset(); + } + + + + boost::shared_ptr MovieAudioFactory::createDecoder(Video::VideoState* videoState) + { + boost::shared_ptr decoder(new MWSound::MovieAudioDecoder(videoState)); + decoder->setupFormat(); + + MWBase::SoundPtr sound = MWBase::Environment::get().getSoundManager()->playTrack(decoder->mDecoderBridge, MWBase::SoundManager::Play_TypeMovie); + if (!sound.get()) + { + decoder.reset(); + return decoder; + } + + decoder->mAudioTrack = sound; + return decoder; + } + +} diff --git a/apps/openmw/mwsound/movieaudiofactory.hpp b/apps/openmw/mwsound/movieaudiofactory.hpp new file mode 100644 index 000000000..a3c602197 --- /dev/null +++ b/apps/openmw/mwsound/movieaudiofactory.hpp @@ -0,0 +1,16 @@ +#ifndef OPENMW_MWSOUND_MOVIEAUDIOFACTORY_H +#define OPENMW_MWSOUND_MOVIEAUDIOFACTORY_H + +#include + +namespace MWSound +{ + + class MovieAudioFactory : public Video::MovieAudioFactory + { + virtual boost::shared_ptr createDecoder(Video::VideoState* videoState); + }; + +} + +#endif diff --git a/apps/openmw/mwsound/openal_output.cpp b/apps/openmw/mwsound/openal_output.cpp index b245b9241..3d2795ce1 100644 --- a/apps/openmw/mwsound/openal_output.cpp +++ b/apps/openmw/mwsound/openal_output.cpp @@ -11,11 +11,16 @@ #include "sound_decoder.hpp" #include "sound.hpp" #include "soundmanagerimp.hpp" +#include "loudness.hpp" #ifndef ALC_ALL_DEVICES_SPECIFIER #define ALC_ALL_DEVICES_SPECIFIER 0x1013 #endif +namespace +{ + const int loudnessFPS = 20; // loudness values per second of audio +} namespace MWSound { @@ -734,7 +739,7 @@ void OpenAL_Output::deinit() mUnusedBuffers.clear(); while(!mBufferCache.empty()) { - alDeleteBuffers(1, &mBufferCache.begin()->second); + alDeleteBuffers(1, &mBufferCache.begin()->second.mALBuffer); mBufferCache.erase(mBufferCache.begin()); } @@ -750,14 +755,14 @@ void OpenAL_Output::deinit() } -ALuint OpenAL_Output::getBuffer(const std::string &fname) +const CachedSound& OpenAL_Output::getBuffer(const std::string &fname) { ALuint buf = 0; NameMap::iterator iditer = mBufferCache.find(fname); if(iditer != mBufferCache.end()) { - buf = iditer->second; + buf = iditer->second.mALBuffer; if(mBufferRefs[buf]++ == 0) { IDDq::iterator iter = std::find(mUnusedBuffers.begin(), @@ -766,7 +771,7 @@ ALuint OpenAL_Output::getBuffer(const std::string &fname) mUnusedBuffers.erase(iter); } - return buf; + return iditer->second; } throwALerror(); @@ -795,12 +800,16 @@ ALuint OpenAL_Output::getBuffer(const std::string &fname) decoder->readAll(data); decoder->close(); + CachedSound cached; + analyzeLoudness(data, srate, chans, type, cached.mLoudnessVector, loudnessFPS); + alGenBuffers(1, &buf); throwALerror(); alBufferData(buf, format, &data[0], data.size(), srate); - mBufferCache[fname] = buf; mBufferRefs[buf] = 1; + cached.mALBuffer = buf; + mBufferCache[fname] = cached; ALint bufsize = 0; alGetBufferi(buf, AL_SIZE, &bufsize); @@ -821,7 +830,7 @@ ALuint OpenAL_Output::getBuffer(const std::string &fname) NameMap::iterator nameiter = mBufferCache.begin(); while(nameiter != mBufferCache.end()) { - if(nameiter->second == oldbuf) + if(nameiter->second.mALBuffer == oldbuf) mBufferCache.erase(nameiter++); else ++nameiter; @@ -832,7 +841,8 @@ ALuint OpenAL_Output::getBuffer(const std::string &fname) alDeleteBuffers(1, &oldbuf); mBufferCacheMemSize -= bufsize; } - return buf; + + return mBufferCache[fname]; } void OpenAL_Output::bufferFinished(ALuint buf) @@ -856,7 +866,7 @@ MWBase::SoundPtr OpenAL_Output::playSound(const std::string &fname, float vol, f try { - buf = getBuffer(fname); + buf = getBuffer(fname).mALBuffer; sound.reset(new OpenAL_Sound(*this, src, buf, Ogre::Vector3(0.0f), vol, basevol, pitch, 1.0f, 1000.0f, flags)); } catch(std::exception&) @@ -883,7 +893,7 @@ MWBase::SoundPtr OpenAL_Output::playSound(const std::string &fname, float vol, f } MWBase::SoundPtr OpenAL_Output::playSound3D(const std::string &fname, const Ogre::Vector3 &pos, float vol, float basevol, float pitch, - float min, float max, int flags, float offset) + float min, float max, int flags, float offset, bool extractLoudness) { boost::shared_ptr sound; ALuint src=0, buf=0; @@ -895,8 +905,12 @@ MWBase::SoundPtr OpenAL_Output::playSound3D(const std::string &fname, const Ogre try { - buf = getBuffer(fname); + const CachedSound& cached = getBuffer(fname); + buf = cached.mALBuffer; + sound.reset(new OpenAL_Sound3D(*this, src, buf, pos, vol, basevol, pitch, min, max, flags)); + if (extractLoudness) + sound->setLoudnessVector(cached.mLoudnessVector, loudnessFPS); } catch(std::exception&) { diff --git a/apps/openmw/mwsound/openal_output.hpp b/apps/openmw/mwsound/openal_output.hpp index 31edf7359..be12bfbec 100644 --- a/apps/openmw/mwsound/openal_output.hpp +++ b/apps/openmw/mwsound/openal_output.hpp @@ -16,6 +16,12 @@ namespace MWSound class SoundManager; class Sound; + struct CachedSound + { + ALuint mALBuffer; + std::vector mLoudnessVector; + }; + class OpenAL_Output : public Sound_Output { ALCdevice *mDevice; @@ -25,7 +31,7 @@ namespace MWSound IDDq mFreeSources; IDDq mUnusedBuffers; - typedef std::map NameMap; + typedef std::map NameMap; NameMap mBufferCache; typedef std::map IDRefMap; @@ -36,7 +42,7 @@ namespace MWSound typedef std::vector SoundVec; SoundVec mActiveSounds; - ALuint getBuffer(const std::string &fname); + const CachedSound& getBuffer(const std::string &fname); void bufferFinished(ALuint buffer); Environment mLastEnvironment; @@ -49,7 +55,7 @@ namespace MWSound virtual MWBase::SoundPtr playSound(const std::string &fname, float vol, float basevol, float pitch, int flags, float offset); /// @param offset Value from [0,1] meaning from which fraction the sound the playback starts. virtual MWBase::SoundPtr playSound3D(const std::string &fname, const Ogre::Vector3 &pos, - float vol, float basevol, float pitch, float min, float max, int flags, float offset); + float vol, float basevol, float pitch, float min, float max, int flags, float offset, bool extractLoudness=false); virtual MWBase::SoundPtr streamSound(DecoderPtr decoder, float volume, float pitch, int flags); virtual void updateListener(const Ogre::Vector3 &pos, const Ogre::Vector3 &atdir, const Ogre::Vector3 &updir, Environment env); diff --git a/apps/openmw/mwsound/sound.cpp b/apps/openmw/mwsound/sound.cpp new file mode 100644 index 000000000..b3105a82c --- /dev/null +++ b/apps/openmw/mwsound/sound.cpp @@ -0,0 +1,23 @@ +#include "sound.hpp" + +namespace MWSound +{ + + float Sound::getCurrentLoudness() + { + if (mLoudnessVector.empty()) + return 0.f; + int index = getTimeOffset() * mLoudnessFPS; + + index = std::max(0, std::min(index, int(mLoudnessVector.size()-1))); + + return mLoudnessVector[index]; + } + + void Sound::setLoudnessVector(const std::vector &loudnessVector, float loudnessFPS) + { + mLoudnessVector = loudnessVector; + mLoudnessFPS = loudnessFPS; + } + +} diff --git a/apps/openmw/mwsound/sound.hpp b/apps/openmw/mwsound/sound.hpp index 670002a30..1b5c00196 100644 --- a/apps/openmw/mwsound/sound.hpp +++ b/apps/openmw/mwsound/sound.hpp @@ -24,6 +24,9 @@ namespace MWSound int mFlags; float mFadeOutTime; + std::vector mLoudnessVector; + float mLoudnessFPS; + public: virtual void stop() = 0; virtual bool isPlaying() = 0; @@ -31,6 +34,12 @@ namespace MWSound void setPosition(const Ogre::Vector3 &pos) { mPos = pos; } void setVolume(float volume) { mVolume = volume; } void setFadeout(float duration) { mFadeOutTime=duration; } + void setLoudnessVector(const std::vector& loudnessVector, float loudnessFPS); + + /// Get loudness at the current time position on a [0,1] scale. + /// Requires that loudnessVector was filled in by the user. + float getCurrentLoudness(); + MWBase::SoundManager::PlayType getPlayType() const { return (MWBase::SoundManager::PlayType)(mFlags&MWBase::SoundManager::Play_TypeMask); } @@ -44,6 +53,7 @@ namespace MWSound , mMaxDistance(maxdist) , mFlags(flags) , mFadeOutTime(0) + , mLoudnessFPS(20) { } virtual ~Sound() { } diff --git a/apps/openmw/mwsound/sound_output.hpp b/apps/openmw/mwsound/sound_output.hpp index 91e25db52..a9a999a5c 100644 --- a/apps/openmw/mwsound/sound_output.hpp +++ b/apps/openmw/mwsound/sound_output.hpp @@ -28,7 +28,7 @@ namespace MWSound virtual MWBase::SoundPtr playSound(const std::string &fname, float vol, float basevol, float pitch, int flags, float offset) = 0; /// @param offset Value from [0,1] meaning from which fraction the sound the playback starts. virtual MWBase::SoundPtr playSound3D(const std::string &fname, const Ogre::Vector3 &pos, - float vol, float basevol, float pitch, float min, float max, int flags, float offset) = 0; + float vol, float basevol, float pitch, float min, float max, int flags, float offset, bool extractLoudness=false) = 0; virtual MWBase::SoundPtr streamSound(DecoderPtr decoder, float volume, float pitch, int flags) = 0; virtual void updateListener(const Ogre::Vector3 &pos, const Ogre::Vector3 &atdir, const Ogre::Vector3 &updir, Environment env) = 0; diff --git a/apps/openmw/mwsound/soundmanagerimp.cpp b/apps/openmw/mwsound/soundmanagerimp.cpp index 4a3093b10..781bfb5d5 100644 --- a/apps/openmw/mwsound/soundmanagerimp.cpp +++ b/apps/openmw/mwsound/soundmanagerimp.cpp @@ -17,15 +17,10 @@ #include "openal_output.hpp" #define SOUND_OUT "OpenAL" -/* Set up the sound manager to use FFMPEG for input. - * The OPENMW_USE_x macros are set in CMakeLists.txt. -*/ -#ifdef OPENMW_USE_FFMPEG #include "ffmpeg_decoder.hpp" #ifndef SOUND_IN #define SOUND_IN "FFmpeg" #endif -#endif namespace MWSound @@ -42,6 +37,7 @@ namespace MWSound , mListenerPos(0,0,0) , mListenerDir(1,0,0) , mListenerUp(0,0,1) + , mListenerUnderwater(false) { if(!useSound) return; @@ -179,6 +175,7 @@ namespace MWSound if(!mOutput->isInitialized()) return; std::cout <<"Playing "<begin(), resourcesInThisGroup->end()); + Ogre::StringVector groups = Ogre::ResourceGroupManager::getSingleton().getResourceGroups (); + for (Ogre::StringVector::iterator it = groups.begin(); it != groups.end(); ++it) + { + Ogre::StringVectorPtr resourcesInThisGroup = mResourceMgr.findResourceNames(*it, + "Music/"+mCurrentPlaylist+"/*"); + filelist.insert(filelist.end(), resourcesInThisGroup->begin(), resourcesInThisGroup->end()); + } + mMusicFiles[mCurrentPlaylist] = filelist; } + else + filelist = mMusicFiles[mCurrentPlaylist]; if(!filelist.size()) return; int i = rand()%filelist.size(); + + // Don't play the same music track twice in a row + if (filelist[i] == mLastPlayedMusic) + { + i = (i+1) % filelist.size(); + } + streamMusicFull(filelist[i]); } @@ -242,7 +251,7 @@ namespace MWSound const Ogre::Vector3 objpos(pos.pos); MWBase::SoundPtr sound = mOutput->playSound3D(filePath, objpos, 1.0f, basevol, 1.0f, - 20.0f, 1500.0f, Play_Normal|Play_TypeVoice, 0); + 20.0f, 1500.0f, Play_Normal|Play_TypeVoice, 0, true); mActiveSounds[sound] = std::make_pair(ptr, std::string("_say_sound")); } catch(std::exception &e) @@ -251,6 +260,21 @@ namespace MWSound } } + float SoundManager::getSaySoundLoudness(const MWWorld::Ptr &ptr) const + { + SoundMap::const_iterator snditer = mActiveSounds.begin(); + while(snditer != mActiveSounds.end()) + { + if(snditer->second.first == ptr && snditer->second.second == "_say_sound") + break; + ++snditer; + } + if (snditer == mActiveSounds.end()) + return 0.f; + + return snditer->first->getCurrentLoudness(); + } + void SoundManager::say(const std::string& filename) { if(!mOutput->isInitialized()) @@ -372,7 +396,7 @@ namespace MWSound sound = mOutput->playSound3D(file, initialPos, volume, basevol, pitch, min, max, mode|type, offset); mActiveSounds[sound] = std::make_pair(MWWorld::Ptr(), soundId); } - catch(std::exception &e) + catch(std::exception &) { //std::cout <<"Sound Error: "<second.first != MWWorld::Ptr() && + snditer->second.first.getCellRef().getRefId() != "player" && snditer->second.first.getCell() == cell) { snditer->first->stop(); @@ -571,12 +596,8 @@ namespace MWSound if(!isMusicPlaying()) startRandomTitle(); - MWWorld::Ptr player = - MWBase::Environment::get().getWorld()->getPlayerPtr(); - const ESM::Cell *cell = player.getCell()->getCell(); - Environment env = Env_Normal; - if((cell->mData.mFlags&cell->HasWater) && mListenerPos.z < cell->mWater) + if (mListenerUnderwater) { env = Env_Underwater; //play underwater sound @@ -669,6 +690,12 @@ namespace MWSound mListenerPos = pos; mListenerDir = dir; mListenerUp = up; + + MWWorld::Ptr player = + MWBase::Environment::get().getWorld()->getPlayerPtr(); + const ESM::Cell *cell = player.getCell()->getCell(); + + mListenerUnderwater = ((cell->mData.mFlags&cell->HasWater) && mListenerPos.z < cell->mWater); } void SoundManager::updatePtr(const MWWorld::Ptr &old, const MWWorld::Ptr &updated) diff --git a/apps/openmw/mwsound/soundmanagerimp.hpp b/apps/openmw/mwsound/soundmanagerimp.hpp index ab9dcf734..250cb0d51 100644 --- a/apps/openmw/mwsound/soundmanagerimp.hpp +++ b/apps/openmw/mwsound/soundmanagerimp.hpp @@ -31,6 +31,10 @@ namespace MWSound std::auto_ptr mOutput; + // Caches available music tracks by + std::map mMusicFiles; + std::string mLastPlayedMusic; // The music file that was last played + float mMasterVolume; float mSFXVolume; float mMusicVolume; @@ -46,6 +50,7 @@ namespace MWSound MWBase::SoundPtr mUnderwaterSound; + bool mListenerUnderwater; Ogre::Vector3 mListenerPos; Ogre::Vector3 mListenerDir; Ogre::Vector3 mListenerUp; @@ -105,6 +110,11 @@ namespace MWSound virtual void stopSay(const MWWorld::Ptr &reference=MWWorld::Ptr()); ///< Stop an actor speaking + virtual float getSaySoundLoudness(const MWWorld::Ptr& reference) const; + ///< Check the currently playing say sound for this actor + /// and get an average loudness value (scale [0,1]) at the current time position. + /// If the actor is not saying anything, returns 0. + virtual MWBase::SoundPtr playTrack(const DecoderPtr& decoder, PlayType type); ///< Play a 2D audio track, using a custom decoder diff --git a/apps/openmw/mwstate/character.cpp b/apps/openmw/mwstate/character.cpp index 5fe80ce0c..f8fcfceec 100644 --- a/apps/openmw/mwstate/character.cpp +++ b/apps/openmw/mwstate/character.cpp @@ -14,9 +14,6 @@ #include -#include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" - bool MWState::operator< (const Slot& left, const Slot& right) { return left.mTimeStamp> index) && index>=mNext) - mNext = index+1; } std::sort (mSlots.begin(), mSlots.end()); diff --git a/apps/openmw/mwstate/character.hpp b/apps/openmw/mwstate/character.hpp index 874533289..4703f0cca 100644 --- a/apps/openmw/mwstate/character.hpp +++ b/apps/openmw/mwstate/character.hpp @@ -26,7 +26,6 @@ namespace MWState boost::filesystem::path mPath; std::vector mSlots; - int mNext; void addSlot (const boost::filesystem::path& path, const std::string& game); diff --git a/apps/openmw/mwstate/charactermanager.cpp b/apps/openmw/mwstate/charactermanager.cpp index 822e2d88e..70e9f0925 100644 --- a/apps/openmw/mwstate/charactermanager.cpp +++ b/apps/openmw/mwstate/charactermanager.cpp @@ -3,12 +3,13 @@ #include #include +#include // std::isalnum #include MWState::CharacterManager::CharacterManager (const boost::filesystem::path& saves, const std::string& game) -: mPath (saves), mNext (0), mCurrent (0), mGame (game) +: mPath (saves), mCurrent (0), mGame (game) { if (!boost::filesystem::is_directory (mPath)) { @@ -28,64 +29,82 @@ MWState::CharacterManager::CharacterManager (const boost::filesystem::path& save if (character.begin()!=character.end()) mCharacters.push_back (character); } - - std::istringstream stream (characterDir.filename().string()); - - int index = 0; - - if ((stream >> index) && index>=mNext) - mNext = index+1; } } } -MWState::Character *MWState::CharacterManager::getCurrentCharacter (bool create) +MWState::Character *MWState::CharacterManager::getCurrentCharacter (bool create, const std::string& name) { if (!mCurrent && create) - createCharacter(); + createCharacter(name); return mCurrent; } void MWState::CharacterManager::deleteSlot(const MWState::Character *character, const MWState::Slot *slot) { - int index = character - &mCharacters[0]; - - if (index<0 || index>=static_cast (mCharacters.size())) - throw std::logic_error ("invalid character"); + std::list::iterator it = findCharacter(character); - mCharacters[index].deleteSlot(slot); + it->deleteSlot(slot); - if (mCharacters[index].begin() == mCharacters[index].end()) + if (character->begin() == character->end()) { // All slots deleted, cleanup and remove this character - mCharacters[index].cleanup(); + it->cleanup(); if (character == mCurrent) mCurrent = NULL; - mCharacters.erase(mCharacters.begin() + index); + mCharacters.erase(it); } } -void MWState::CharacterManager::createCharacter() +void MWState::CharacterManager::createCharacter(const std::string& name) { std::ostringstream stream; - stream << mNext++; + + // The character name is user-supplied, so we need to escape the path + for (std::string::const_iterator it = name.begin(); it != name.end(); ++it) + { + if (std::isalnum(*it)) // Ignores multibyte characters and non alphanumeric characters + stream << *it; + else + stream << "_"; + } boost::filesystem::path path = mPath / stream.str(); + // Append an index if necessary to ensure a unique directory + int i=0; + while (boost::filesystem::exists(path)) + { + std::ostringstream test; + test << stream.str(); + test << " - " << ++i; + path = mPath / test.str(); + } + mCharacters.push_back (Character (path, mGame)); mCurrent = &mCharacters.back(); } -void MWState::CharacterManager::setCurrentCharacter (const Character *character) +std::list::iterator MWState::CharacterManager::findCharacter(const MWState::Character* character) { - int index = character - &mCharacters[0]; - - if (index<0 || index>=static_cast (mCharacters.size())) + std::list::iterator it = mCharacters.begin(); + for (; it != mCharacters.end(); ++it) + { + if (&*it == character) + break; + } + if (it == mCharacters.end()) throw std::logic_error ("invalid character"); + return it; +} + +void MWState::CharacterManager::setCurrentCharacter (const Character *character) +{ + std::list::iterator it = findCharacter(character); - mCurrent = &mCharacters[index]; + mCurrent = &*it; } void MWState::CharacterManager::clearCurrentCharacter() @@ -93,12 +112,12 @@ void MWState::CharacterManager::clearCurrentCharacter() mCurrent = 0; } -std::vector::const_iterator MWState::CharacterManager::begin() const +std::list::const_iterator MWState::CharacterManager::begin() const { return mCharacters.begin(); } -std::vector::const_iterator MWState::CharacterManager::end() const +std::list::const_iterator MWState::CharacterManager::end() const { return mCharacters.end(); } diff --git a/apps/openmw/mwstate/charactermanager.hpp b/apps/openmw/mwstate/charactermanager.hpp index 869d34f21..c44c10b5a 100644 --- a/apps/openmw/mwstate/charactermanager.hpp +++ b/apps/openmw/mwstate/charactermanager.hpp @@ -10,8 +10,10 @@ namespace MWState class CharacterManager { boost::filesystem::path mPath; - int mNext; - std::vector mCharacters; + + // Uses std::list, so that mCurrent stays valid when characters are deleted + std::list mCharacters; + Character *mCurrent; std::string mGame; @@ -23,25 +25,29 @@ namespace MWState CharacterManager& operator= (const CharacterManager&); ///< Not implemented + std::list::iterator findCharacter(const MWState::Character* character); + public: CharacterManager (const boost::filesystem::path& saves, const std::string& game); - Character *getCurrentCharacter (bool create = true); + Character *getCurrentCharacter (bool create, const std::string& name); ///< \param create Create a new character, if there is no current character. + /// \param name The character name to use in case a new character is created. void deleteSlot(const MWState::Character *character, const MWState::Slot *slot); - void createCharacter(); + void createCharacter(const std::string& name); ///< Create new character within saved game management + /// \param name Name for the character (does not need to be unique) void setCurrentCharacter (const Character *character); void clearCurrentCharacter(); - std::vector::const_iterator begin() const; + std::list::const_iterator begin() const; - std::vector::const_iterator end() const; + std::list::const_iterator end() const; }; } diff --git a/apps/openmw/mwstate/statemanagerimp.cpp b/apps/openmw/mwstate/statemanagerimp.cpp index dafb8323a..18ebe11ce 100644 --- a/apps/openmw/mwstate/statemanagerimp.cpp +++ b/apps/openmw/mwstate/statemanagerimp.cpp @@ -45,6 +45,7 @@ void MWState::StateManager::cleanup (bool force) MWBase::Environment::get().getWorld()->clear(); MWBase::Environment::get().getWindowManager()->clear(); MWBase::Environment::get().getInputManager()->clear(); + MWBase::Environment::get().getMechanicsManager()->clear(); mState = State_NoGame; mCharacterManager.clearCurrentCharacter(); @@ -131,12 +132,12 @@ void MWState::StateManager::newGame (bool bypass) { cleanup(); - MWBase::Environment::get().getWorld()->startNewGame (bypass); - if (!bypass) MWBase::Environment::get().getWindowManager()->setNewGame (true); - else - MWBase::Environment::get().getWorld()->setGlobalInt ("chargenstate", -1); + + MWBase::Environment::get().getScriptManager()->getGlobalScripts().addStartup(); + + MWBase::Environment::get().getWorld()->startNewGame (bypass); mState = State_Running; } @@ -144,96 +145,125 @@ void MWState::StateManager::newGame (bool bypass) void MWState::StateManager::endGame() { mState = State_Ended; - MWBase::Environment::get().getWorld()->useDeathCamera(); } void MWState::StateManager::saveGame (const std::string& description, const Slot *slot) { - ESM::SavedGame profile; + try + { + ESM::SavedGame profile; - MWBase::World& world = *MWBase::Environment::get().getWorld(); + MWBase::World& world = *MWBase::Environment::get().getWorld(); - MWWorld::Ptr player = world.getPlayerPtr(); + MWWorld::Ptr player = world.getPlayerPtr(); - profile.mContentFiles = world.getContentFiles(); + profile.mContentFiles = world.getContentFiles(); - profile.mPlayerName = player.getClass().getName (player); - profile.mPlayerLevel = player.getClass().getNpcStats (player).getLevel(); + profile.mPlayerName = player.get()->mBase->mName; + profile.mPlayerLevel = player.getClass().getNpcStats (player).getLevel(); - std::string classId = player.get()->mBase->mClass; - if (world.getStore().get().isDynamic(classId)) - profile.mPlayerClassName = world.getStore().get().find(classId)->mName; - else - profile.mPlayerClassId = classId; + std::string classId = player.get()->mBase->mClass; + if (world.getStore().get().isDynamic(classId)) + profile.mPlayerClassName = world.getStore().get().find(classId)->mName; + else + profile.mPlayerClassId = classId; + + profile.mPlayerCell = world.getCellName(); + + profile.mInGameTime.mGameHour = world.getTimeStamp().getHour(); + profile.mInGameTime.mDay = world.getDay(); + profile.mInGameTime.mMonth = world.getMonth(); + profile.mInGameTime.mYear = world.getYear(); + profile.mTimePlayed = mTimePlayed; + profile.mDescription = description; + + int screenshotW = 259*2, screenshotH = 133*2; // *2 to get some nice antialiasing + Ogre::Image screenshot; + world.screenshot(screenshot, screenshotW, screenshotH); + Ogre::DataStreamPtr encoded = screenshot.encode("jpg"); + profile.mScreenshot.resize(encoded->size()); + encoded->read(&profile.mScreenshot[0], encoded->size()); + + if (!slot) + slot = getCurrentCharacter()->createSlot (profile); + else + slot = getCurrentCharacter()->updateSlot (slot, profile); - profile.mPlayerCell = world.getCellName(); + boost::filesystem::ofstream stream (slot->mPath, std::ios::binary); - profile.mInGameTime.mGameHour = world.getTimeStamp().getHour(); - profile.mInGameTime.mDay = world.getDay(); - profile.mInGameTime.mMonth = world.getMonth(); - profile.mInGameTime.mYear = world.getYear(); - profile.mTimePlayed = mTimePlayed; - profile.mDescription = description; + ESM::ESMWriter writer; - int screenshotW = 259*2, screenshotH = 133*2; // *2 to get some nice antialiasing - Ogre::Image screenshot; - world.screenshot(screenshot, screenshotW, screenshotH); - Ogre::DataStreamPtr encoded = screenshot.encode("jpg"); - profile.mScreenshot.resize(encoded->size()); - encoded->read(&profile.mScreenshot[0], encoded->size()); + const std::vector& current = + MWBase::Environment::get().getWorld()->getContentFiles(); - if (!slot) - slot = mCharacterManager.getCurrentCharacter()->createSlot (profile); - else - slot = mCharacterManager.getCurrentCharacter()->updateSlot (slot, profile); + for (std::vector::const_iterator iter (current.begin()); iter!=current.end(); + ++iter) + writer.addMaster (*iter, 0); // not using the size information anyway -> use value of 0 - boost::filesystem::ofstream stream (slot->mPath, std::ios::binary); + writer.setFormat (ESM::Header::CurrentFormat); - ESM::ESMWriter writer; + // all unused + writer.setVersion(0); + writer.setType(0); + writer.setAuthor(""); + writer.setDescription(""); - const std::vector& current = - MWBase::Environment::get().getWorld()->getContentFiles(); + int recordCount = 1 // saved game header + +MWBase::Environment::get().getJournal()->countSavedGameRecords() + +MWBase::Environment::get().getWorld()->countSavedGameRecords() + +MWBase::Environment::get().getScriptManager()->getGlobalScripts().countSavedGameRecords() + +MWBase::Environment::get().getDialogueManager()->countSavedGameRecords() + +MWBase::Environment::get().getWindowManager()->countSavedGameRecords() + +MWBase::Environment::get().getMechanicsManager()->countSavedGameRecords(); + writer.setRecordCount (recordCount); - for (std::vector::const_iterator iter (current.begin()); iter!=current.end(); - ++iter) - writer.addMaster (*iter, 0); // not using the size information anyway -> use value of 0 + writer.save (stream); - writer.setFormat (ESM::Header::CurrentFormat); - int recordCount = 1 // saved game header - +MWBase::Environment::get().getJournal()->countSavedGameRecords() - +MWBase::Environment::get().getWorld()->countSavedGameRecords() - +MWBase::Environment::get().getScriptManager()->getGlobalScripts().countSavedGameRecords() - +MWBase::Environment::get().getDialogueManager()->countSavedGameRecords() - +MWBase::Environment::get().getWindowManager()->countSavedGameRecords(); - writer.setRecordCount (recordCount); + Loading::Listener& listener = *MWBase::Environment::get().getWindowManager()->getLoadingScreen(); + listener.setProgressRange(recordCount); + listener.setLabel("#{sNotifyMessage4}"); - writer.save (stream); + Loading::ScopedLoad load(&listener); - Loading::Listener& listener = *MWBase::Environment::get().getWindowManager()->getLoadingScreen(); - listener.setProgressRange(recordCount); - listener.setLabel("#{sNotifyMessage4}"); + writer.startRecord (ESM::REC_SAVE); + slot->mProfile.save (writer); + writer.endRecord (ESM::REC_SAVE); + listener.increaseProgress(); - Loading::ScopedLoad load(&listener); + MWBase::Environment::get().getJournal()->write (writer, listener); + MWBase::Environment::get().getDialogueManager()->write (writer, listener); + MWBase::Environment::get().getWorld()->write (writer, listener); + MWBase::Environment::get().getScriptManager()->getGlobalScripts().write (writer, listener); + MWBase::Environment::get().getWindowManager()->write(writer, listener); + MWBase::Environment::get().getMechanicsManager()->write(writer, listener); - writer.startRecord (ESM::REC_SAVE); - slot->mProfile.save (writer); - writer.endRecord (ESM::REC_SAVE); - listener.increaseProgress(); + // Ensure we have written the number of records that was estimated + if (writer.getRecordCount() != recordCount+1) // 1 extra for TES3 record + std::cerr << "Warning: number of written savegame records does not match. Estimated: " << recordCount+1 << ", written: " << writer.getRecordCount() << std::endl; - MWBase::Environment::get().getJournal()->write (writer, listener); - MWBase::Environment::get().getDialogueManager()->write (writer, listener); - MWBase::Environment::get().getWorld()->write (writer, listener); - MWBase::Environment::get().getScriptManager()->getGlobalScripts().write (writer, listener); - MWBase::Environment::get().getWindowManager()->write(writer, listener); + writer.close(); - // Ensure we have written the number of records that was estimated - if (writer.getRecordCount() != recordCount+1) // 1 extra for TES3 record - std::cerr << "Warning: number of written savegame records does not match. Estimated: " << recordCount+1 << ", written: " << writer.getRecordCount() << std::endl; + if (stream.fail()) + throw std::runtime_error("Write operation failed"); - writer.close(); + Settings::Manager::setString ("character", "Saves", + slot->mPath.parent_path().filename().string()); + } + catch (const std::exception& e) + { + std::stringstream error; + error << "Failed to save game: " << e.what(); + + std::cerr << error.str() << std::endl; - Settings::Manager::setString ("character", "Saves", - slot->mPath.parent_path().filename().string()); + std::vector buttons; + buttons.push_back("#{sOk}"); + MWBase::Environment::get().getWindowManager()->messageBox(error.str(), buttons); + + // If no file was written, clean up the slot + if (slot && !boost::filesystem::exists(slot->mPath)) + getCurrentCharacter()->deleteSlot(slot); + } } void MWState::StateManager::quickSave (std::string name) @@ -321,6 +351,7 @@ void MWState::StateManager::loadGame (const Character *character, const Slot *sl case ESM::REC_ACTC: case ESM::REC_PROJ: case ESM::REC_MPRJ: + case ESM::REC_ENAB: MWBase::Environment::get().getWorld()->readRecord (reader, n.val, contentFileMap); break; @@ -333,14 +364,20 @@ void MWState::StateManager::loadGame (const Character *character, const Slot *sl case ESM::REC_GMAP: case ESM::REC_KEYS: case ESM::REC_ASPL: + case ESM::REC_MARK: MWBase::Environment::get().getWindowManager()->readRecord(reader, n.val); break; + case ESM::REC_DCOU: + + MWBase::Environment::get().getMechanicsManager()->readRecord(reader, n.val); + break; + default: // ignore invalid records - /// \todo log error + std::cerr << "Ignoring unknown record: " << n.name << std::endl; reader.skipRecord(); } listener.increaseProgress(); @@ -366,15 +403,24 @@ void MWState::StateManager::loadGame (const Character *character, const Slot *sl // Use detectWorldSpaceChange=false, otherwise some of the data we just loaded would be cleared again MWBase::Environment::get().getWorld()->changeToCell (cellId, ptr.getRefData().getPosition(), false); + MWBase::Environment::get().getScriptManager()->getGlobalScripts().addStartup(); + // Do not trigger erroneous cellChanged events MWBase::Environment::get().getWorld()->markCellAsUnchanged(); } catch (const std::exception& e) { - std::cerr << "failed to load saved game: " << e.what() << std::endl; + std::stringstream error; + error << "Failed to load saved game: " << e.what(); + + std::cerr << error.str() << std::endl; cleanup (true); MWBase::Environment::get().getWindowManager()->pushGuiMode (MWGui::GM_MainMenu); + + std::vector buttons; + buttons.push_back("#{sOk}"); + MWBase::Environment::get().getWindowManager()->messageBox(error.str(), buttons); } } @@ -392,7 +438,10 @@ void MWState::StateManager::deleteGame(const MWState::Character *character, cons MWState::Character *MWState::StateManager::getCurrentCharacter (bool create) { - return mCharacterManager.getCurrentCharacter (create); + MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); + std::string name = player.get()->mBase->mName; + + return mCharacterManager.getCurrentCharacter (create, name); } MWState::StateManager::CharacterIterator MWState::StateManager::characterBegin() @@ -413,11 +462,12 @@ void MWState::StateManager::update (float duration) if (mAskLoadRecent) { int iButton = MWBase::Environment::get().getWindowManager()->readPressedButton(); - if(iButton==0) + MWState::Character *curCharacter = getCurrentCharacter(false); + if(iButton==0 && curCharacter) { mAskLoadRecent = false; //Load last saved game for current character - MWState::Character *curCharacter = getCurrentCharacter(); + MWState::Slot lastSave = *curCharacter->begin(); loadGame(curCharacter, &lastSave); } diff --git a/apps/openmw/mwworld/actionapply.cpp b/apps/openmw/mwworld/actionapply.cpp index 6b12cc3e6..bfd64c85d 100644 --- a/apps/openmw/mwworld/actionapply.cpp +++ b/apps/openmw/mwworld/actionapply.cpp @@ -6,30 +6,36 @@ #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" +#include "../mwworld/containerstore.hpp" + namespace MWWorld { - ActionApply::ActionApply (const Ptr& target, const std::string& id) - : Action (false, target), mId (id) + ActionApply::ActionApply (const Ptr& object, const std::string& id) + : Action (false, object), mId (id) {} void ActionApply::executeImp (const Ptr& actor) { MWBase::Environment::get().getWorld()->breakInvisibility(actor); - getTarget().getClass().apply (getTarget(), mId, actor); + actor.getClass().apply (actor, mId, actor); + + actor.getClass().getContainerStore(actor).remove(getTarget(), 1, actor); } - ActionApplyWithSkill::ActionApplyWithSkill (const Ptr& target, const std::string& id, + ActionApplyWithSkill::ActionApplyWithSkill (const Ptr& object, const std::string& id, int skillIndex, int usageType) - : Action (false, target), mId (id), mSkillIndex (skillIndex), mUsageType (usageType) + : Action (false, object), mId (id), mSkillIndex (skillIndex), mUsageType (usageType) {} void ActionApplyWithSkill::executeImp (const Ptr& actor) { MWBase::Environment::get().getWorld()->breakInvisibility(actor); - if (getTarget().getClass().apply (getTarget(), mId, actor) && mUsageType!=-1) - getTarget().getClass().skillUsageSucceeded (actor, mSkillIndex, mUsageType); + if (actor.getClass().apply (actor, mId, actor) && mUsageType!=-1) + actor.getClass().skillUsageSucceeded (actor, mSkillIndex, mUsageType); + + actor.getClass().getContainerStore(actor).remove(getTarget(), 1, actor); } } diff --git a/apps/openmw/mwworld/actionapply.hpp b/apps/openmw/mwworld/actionapply.hpp index 669a19067..5294745a6 100644 --- a/apps/openmw/mwworld/actionapply.hpp +++ b/apps/openmw/mwworld/actionapply.hpp @@ -16,7 +16,7 @@ namespace MWWorld public: - ActionApply (const Ptr& target, const std::string& id); + ActionApply (const Ptr& object, const std::string& id); }; class ActionApplyWithSkill : public Action @@ -29,7 +29,7 @@ namespace MWWorld public: - ActionApplyWithSkill (const Ptr& target, const std::string& id, + ActionApplyWithSkill (const Ptr& object, const std::string& id, int skillIndex, int usageType); }; } diff --git a/apps/openmw/mwworld/actionequip.cpp b/apps/openmw/mwworld/actionequip.cpp index 05677cdc7..50da1e5e5 100644 --- a/apps/openmw/mwworld/actionequip.cpp +++ b/apps/openmw/mwworld/actionequip.cpp @@ -41,6 +41,8 @@ namespace MWWorld // slots that this item can be equipped in std::pair, bool> slots_ = getTarget().getClass().getEquipmentSlots(getTarget()); + if (slots_.first.empty()) + return; // retrieve ContainerStoreIterator to the item MWWorld::ContainerStoreIterator it = invStore.begin(); @@ -55,25 +57,32 @@ namespace MWWorld assert(it != invStore.end()); // equip the item in the first free slot - for (std::vector::const_iterator slot=slots_.first.begin(); - slot!=slots_.first.end(); ++slot) + std::vector::const_iterator slot=slots_.first.begin(); + for (;slot!=slots_.first.end(); ++slot) { // if the item is equipped already, nothing to do if (invStore.getSlot(*slot) == it) return; - // if all slots are occupied, replace the last slot - if (slot == --slots_.first.end()) + if (invStore.getSlot(*slot) == invStore.end()) { + // slot is not occupied invStore.equip(*slot, it, actor); break; } + } - if (invStore.getSlot(*slot) == invStore.end()) + // all slots are occupied -> cycle + // move all slots one towards begin(), then equip the item in the slot that is now free + if (slot == slots_.first.end()) + { + for (slot=slots_.first.begin();slot!=slots_.first.end(); ++slot) { - // slot is not occupied - invStore.equip(*slot, it, actor); - break; + invStore.unequipSlot(*slot, actor); + if (slot+1 != slots_.first.end()) + invStore.equip(*slot, invStore.getSlot(*(slot+1)), actor); + else + invStore.equip(*slot, it, actor); } } } diff --git a/apps/openmw/mwworld/actionteleport.cpp b/apps/openmw/mwworld/actionteleport.cpp index 4378e179d..7fd6ba024 100644 --- a/apps/openmw/mwworld/actionteleport.cpp +++ b/apps/openmw/mwworld/actionteleport.cpp @@ -1,4 +1,3 @@ - #include "actionteleport.hpp" #include "../mwbase/environment.hpp" @@ -6,6 +5,23 @@ #include "../mwbase/mechanicsmanager.hpp" #include "player.hpp" +namespace +{ + + void getFollowers (const MWWorld::Ptr& actor, std::set& out) + { + std::list followers = MWBase::Environment::get().getMechanicsManager()->getActorsFollowing(actor); + for(std::list::iterator it = followers.begin();it != followers.end();++it) + { + if (out.insert(*it).second) + { + getFollowers(*it, out); + } + } + } + +} + namespace MWWorld { ActionTeleport::ActionTeleport (const std::string& cellName, @@ -16,15 +32,20 @@ namespace MWWorld void ActionTeleport::executeImp (const Ptr& actor) { - MWBase::World* world = MWBase::Environment::get().getWorld(); - //find any NPC that is following the actor and teleport him too - std::list followers = MWBase::Environment::get().getMechanicsManager()->getActorsFollowing(actor); - for(std::list::iterator it = followers.begin();it != followers.end();++it) + std::set followers; + getFollowers(actor, followers); + for(std::set::iterator it = followers.begin();it != followers.end();++it) { - executeImp(*it); + teleport(*it); } + teleport(actor); + } + + void ActionTeleport::teleport(const Ptr &actor) + { + MWBase::World* world = MWBase::Environment::get().getWorld(); if(actor == world->getPlayerPtr()) { world->getPlayer().setTeleported(true); diff --git a/apps/openmw/mwworld/actionteleport.hpp b/apps/openmw/mwworld/actionteleport.hpp index a13cb61b2..9ca664de8 100644 --- a/apps/openmw/mwworld/actionteleport.hpp +++ b/apps/openmw/mwworld/actionteleport.hpp @@ -14,12 +14,16 @@ namespace MWWorld std::string mCellName; ESM::Position mPosition; + /// Teleports this actor and also teleports anyone following that actor. virtual void executeImp (const Ptr& actor); + /// Teleports only the given actor (internal use). + void teleport(const Ptr &actor); + public: ActionTeleport (const std::string& cellName, const ESM::Position& position); - ///< If cellName is empty, an exterior cell is asumed. + ///< If cellName is empty, an exterior cell is assumed. }; } diff --git a/apps/openmw/mwworld/cellref.cpp b/apps/openmw/mwworld/cellref.cpp index f16d8e3d1..c0b3bb6af 100644 --- a/apps/openmw/mwworld/cellref.cpp +++ b/apps/openmw/mwworld/cellref.cpp @@ -10,6 +10,12 @@ namespace MWWorld return mCellRef.mRefNum; } + void CellRef::unsetRefNum() + { + mCellRef.mRefNum.mContentFile = -1; + mCellRef.mRefNum.mIndex = 0; + } + std::string CellRef::getRefId() const { return mCellRef.mRefID; @@ -88,6 +94,16 @@ namespace MWWorld return mCellRef.mOwner; } + std::string CellRef::getGlobalVariable() const + { + return mCellRef.mGlobalVariable; + } + + int CellRef::getFactionRank() const + { + return mCellRef.mFactionRank; + } + void CellRef::setOwner(const std::string &owner) { if (owner != mCellRef.mOwner) diff --git a/apps/openmw/mwworld/cellref.hpp b/apps/openmw/mwworld/cellref.hpp index 4db362b1e..d2e4fdf1c 100644 --- a/apps/openmw/mwworld/cellref.hpp +++ b/apps/openmw/mwworld/cellref.hpp @@ -25,6 +25,9 @@ namespace MWWorld // Note: Currently unused for items in containers ESM::RefNum getRefNum() const; + // Set RefNum to its default state. + void unsetRefNum(); + // Id of object being referenced std::string getRefId() const; @@ -42,7 +45,8 @@ namespace MWWorld float getScale() const; void setScale(float scale); - // Position and rotation of this object within the cell + // The *original* position and rotation as it was given in the Construction Set. + // Current position and rotation of the object is stored in RefData. ESM::Position getPosition() const; void setPosition (const ESM::Position& position); @@ -60,6 +64,11 @@ namespace MWWorld std::string getOwner() const; void setOwner(const std::string& owner); + // Name of a global variable. If the global variable is set to '1', using the object is temporarily allowed + // even if it has an Owner field. + // Used by bed rent scripts to allow the player to use the bed for the duration of the rent. + std::string getGlobalVariable() const; + // ID of creature trapped in this soul gem std::string getSoul() const; void setSoul(const std::string& soul); @@ -69,6 +78,9 @@ namespace MWWorld std::string getFaction() const; void setFaction (const std::string& faction); + // PC faction rank required to use the item. Sometimes is -1, which means "any rank". + int getFactionRank() const; + // Lock level for doors and containers // Positive for a locked door. 0 for a door that was never locked. // For an unlocked door, it is set to -(previous locklevel) diff --git a/apps/openmw/mwworld/cells.cpp b/apps/openmw/mwworld/cells.cpp index 3b758f866..ef3d299a9 100644 --- a/apps/openmw/mwworld/cells.cpp +++ b/apps/openmw/mwworld/cells.cpp @@ -193,8 +193,10 @@ MWWorld::Ptr MWWorld::Cells::getPtr (const std::string& name) } // Then check cells that are already listed - for (std::map, CellStore>::iterator iter = mExteriors.begin(); - iter!=mExteriors.end(); ++iter) + // Search in reverse, this is a workaround for an ambiguous chargen_plank reference in the vanilla game. + // there is one at -22,16 and one at -2,-9, the latter should be used. + for (std::map, CellStore>::reverse_iterator iter = mExteriors.rbegin(); + iter!=mExteriors.rend(); ++iter) { Ptr ptr = getPtrAndCache (name, iter->second); if (!ptr.isEmpty()) @@ -314,6 +316,8 @@ bool MWWorld::Cells::readRecord (ESM::ESMReader& reader, int32_t type, catch (...) { // silently drop cells that don't exist anymore + reader.skipRecord(); + return true; /// \todo log } diff --git a/apps/openmw/mwworld/cellstore.cpp b/apps/openmw/mwworld/cellstore.cpp index 63cdbfb1a..ce8d77758 100644 --- a/apps/openmw/mwworld/cellstore.cpp +++ b/apps/openmw/mwworld/cellstore.cpp @@ -410,6 +410,9 @@ namespace MWWorld loadRefs (store, esm); mState = State_Loaded; + + // TODO: the pathgrid graph only needs to be loaded for active cells, so move this somewhere else. + // In a simple test, loading the graph for all cells in MW + expansions took 200 ms mPathgridGraph.load(mCell); } } @@ -447,10 +450,25 @@ namespace MWWorld if (deleted) continue; + // Don't list reference if it was moved to a different cell. + ESM::MovedCellRefTracker::const_iterator iter = + std::find(mCell->mMovedRefs.begin(), mCell->mMovedRefs.end(), ref.mRefNum); + if (iter != mCell->mMovedRefs.end()) { + continue; + } + mIds.push_back (Misc::StringUtils::lowerCase (ref.mRefID)); } } + // List moved references, from separately tracked list. + for (ESM::CellRefTracker::const_iterator it = mCell->mLeasedRefs.begin(); it != mCell->mLeasedRefs.end(); ++it) + { + ESM::CellRef &ref = const_cast(*it); + + mIds.push_back(Misc::StringUtils::lowerCase(ref.mRefID)); + } + std::sort (mIds.begin(), mIds.end()); } diff --git a/apps/openmw/mwworld/cellstore.hpp b/apps/openmw/mwworld/cellstore.hpp index ba6fad7ba..e322ef4a4 100644 --- a/apps/openmw/mwworld/cellstore.hpp +++ b/apps/openmw/mwworld/cellstore.hpp @@ -149,6 +149,17 @@ namespace MWWorld forEachImp (functor, mCreatureLists); } + template + bool forEachContainer (Functor& functor) + { + mHasState = true; + + return + forEachImp (functor, mContainers) && + forEachImp (functor, mCreatures) && + forEachImp (functor, mNpcs); + } + bool isExterior() const; Ptr searchInContainer (const std::string& id); diff --git a/apps/openmw/mwworld/class.cpp b/apps/openmw/mwworld/class.cpp index c13ecfab5..16c2469c1 100644 --- a/apps/openmw/mwworld/class.cpp +++ b/apps/openmw/mwworld/class.cpp @@ -52,7 +52,7 @@ namespace MWWorld return false; } - void Class::skillUsageSucceeded (const MWWorld::Ptr& ptr, int skill, int usageType) const + void Class::skillUsageSucceeded (const MWWorld::Ptr& ptr, int skill, int usageType, float extraFactor) const { throw std::runtime_error ("class does not represent an actor"); } @@ -303,10 +303,6 @@ namespace MWWorld { } - void Class::adjustRotation(const MWWorld::Ptr& ptr,float& x,float& y,float& z) const - { - } - std::string Class::getModel(const MWWorld::Ptr &ptr) const { return ""; @@ -322,7 +318,7 @@ namespace MWWorld return std::make_pair (1, ""); } - void Class::adjustPosition(const MWWorld::Ptr& ptr) const + void Class::adjustPosition(const MWWorld::Ptr& ptr, bool force) const { } @@ -358,7 +354,7 @@ namespace MWWorld Class::copyToCell(const Ptr &ptr, CellStore &cell) const { Ptr newPtr = copyToCellImpl(ptr, cell); - + newPtr.getCellRef().unsetRefNum(); // This RefNum is only valid within the original cell of the reference return newPtr; } @@ -366,7 +362,7 @@ namespace MWWorld Class::copyToCell(const Ptr &ptr, CellStore &cell, const ESM::Position &pos) const { Ptr newPtr = copyToCell(ptr, cell); - newPtr.getRefData().getPosition() = pos; + newPtr.getRefData().setPosition(pos); return newPtr; } @@ -424,4 +420,18 @@ namespace MWWorld { throw std::runtime_error("this is not a door"); } + + float Class::getNormalizedEncumbrance(const Ptr &ptr) const + { + float capacity = getCapacity(ptr); + if (capacity == 0) + return 1.f; + + return getEncumbrance(ptr) / capacity; + } + + std::string Class::getSound(const MWWorld::Ptr&) const + { + return std::string(); + } } diff --git a/apps/openmw/mwworld/class.hpp b/apps/openmw/mwworld/class.hpp index c3f94d7f1..b66ca7488 100644 --- a/apps/openmw/mwworld/class.hpp +++ b/apps/openmw/mwworld/class.hpp @@ -72,12 +72,6 @@ namespace MWWorld public: - /// NPC-stances. - enum Stance - { - Run, Sneak - }; - virtual ~Class(); const std::string& getTypeName() const { @@ -96,12 +90,13 @@ namespace MWWorld ///< \return name (the one that is to be presented to the user; not the internal one); /// can return an empty string. - virtual void adjustPosition(const MWWorld::Ptr& ptr) const; + virtual void adjustPosition(const MWWorld::Ptr& ptr, bool force) const; ///< Adjust position to stand on ground. Must be called post model load + /// @param force do this even if the ptr is flying virtual MWMechanics::CreatureStats& getCreatureStats (const Ptr& ptr) const; ///< Return creature stats or throw an exception, if class does not have creature stats - /// (default implementation: throw an exceoption) + /// (default implementation: throw an exception) virtual bool hasToolTip (const Ptr& ptr) const; ///< @return true if this object has a tooltip when focused (default implementation: false) @@ -111,7 +106,7 @@ namespace MWWorld virtual MWMechanics::NpcStats& getNpcStats (const Ptr& ptr) const; ///< Return NPC stats or throw an exception, if class does not have NPC stats - /// (default implementation: throw an exceoption) + /// (default implementation: throw an exception) virtual bool hasItemHealth (const Ptr& ptr) const; ///< \return Item health data available? (default implementation: false) @@ -128,7 +123,7 @@ namespace MWWorld /// of the given attacker, and whoever is hit. /// \param type - type of attack, one of the MWMechanics::CreatureStats::AttackType /// enums. ignored for creature attacks. - /// (default implementation: throw an exceoption) + /// (default implementation: throw an exception) virtual void onHit(const MWWorld::Ptr &ptr, float damage, bool ishealth, const MWWorld::Ptr &object, const MWWorld::Ptr &attacker, bool successful) const; ///< Alerts \a ptr that it's being hit for \a damage points to health if \a ishealth is @@ -144,7 +139,7 @@ namespace MWWorld ///< Sets a new current health value for the actor, optionally specifying the object causing /// the change. Use this instead of using CreatureStats directly as this will make sure the /// correct dialog and actor states are properly handled when being hurt or healed. - /// (default implementation: throw an exceoption) + /// (default implementation: throw an exception) virtual boost::shared_ptr activate (const Ptr& ptr, const Ptr& actor) const; ///< Generate action for activation (default implementation: return a null action). @@ -156,11 +151,11 @@ namespace MWWorld virtual ContainerStore& getContainerStore (const Ptr& ptr) const; ///< Return container store or throw an exception, if class does not have a - /// container store (default implementation: throw an exceoption) + /// container store (default implementation: throw an exception) virtual InventoryStore& getInventoryStore (const Ptr& ptr) const; ///< Return inventory store or throw an exception, if class does not have a - /// inventory store (default implementation: throw an exceoption) + /// inventory store (default implementation: throw an exception) virtual bool hasInventoryStore (const Ptr& ptr) const; ///< Does this object have an inventory store, i.e. equipment slots? (default implementation: false) @@ -228,6 +223,9 @@ namespace MWWorld /// effects). Throws an exception, if the object can't hold other objects. /// (default implementation: throws an exception) + virtual float getNormalizedEncumbrance (const MWWorld::Ptr& ptr) const; + ///< Returns encumbrance re-scaled to capacity + virtual bool apply (const MWWorld::Ptr& ptr, const std::string& id, const MWWorld::Ptr& actor) const; ///< Apply \a id on \a ptr. @@ -236,7 +234,7 @@ namespace MWWorld /// /// (default implementation: ignore and return false) - virtual void skillUsageSucceeded (const MWWorld::Ptr& ptr, int skill, int usageType) const; + virtual void skillUsageSucceeded (const MWWorld::Ptr& ptr, int skill, int usageType, float extraFactor=1.f) const; ///< Inform actor \a ptr that a skill use has succeeded. /// /// (default implementations: throws an exception) @@ -272,8 +270,6 @@ namespace MWWorld virtual void adjustScale(const MWWorld::Ptr& ptr,float& scale) const; - virtual void adjustRotation(const MWWorld::Ptr& ptr,float& x,float& y,float& z) const; - virtual bool canSell (const MWWorld::Ptr& item, int npcServices) const; ///< Determine whether or not \a item can be sold to an npc with the given \a npcServices @@ -343,6 +339,9 @@ namespace MWWorld virtual void respawn (const MWWorld::Ptr& ptr) const {} virtual void restock (const MWWorld::Ptr& ptr) const {} + + /// Returns sound id + virtual std::string getSound(const MWWorld::Ptr& ptr) const; }; } diff --git a/apps/openmw/mwworld/containerstore.cpp b/apps/openmw/mwworld/containerstore.cpp index 8a076c3fc..45728371b 100644 --- a/apps/openmw/mwworld/containerstore.cpp +++ b/apps/openmw/mwworld/containerstore.cpp @@ -92,6 +92,8 @@ void MWWorld::ContainerStore::storeStates (const CellRefList& collection, for (typename CellRefList::List::const_iterator iter (collection.mList.begin()); iter!=collection.mList.end(); ++iter) { + if (iter->mData.getCount() == 0) + continue; ESM::ObjectState state; storeState (*iter, state); int slot = equipable ? getSlot (*iter) : -1; @@ -139,6 +141,34 @@ void MWWorld::ContainerStore::unstack(const Ptr &ptr, const Ptr& container) remove(ptr, ptr.getRefData().getCount()-1, container); } +MWWorld::ContainerStoreIterator MWWorld::ContainerStore::restack(const MWWorld::Ptr& item) +{ + MWWorld::ContainerStoreIterator retval = end(); + for (MWWorld::ContainerStoreIterator iter (begin()); iter != end(); ++iter) + { + if (item == *iter) + { + retval = iter; + break; + } + } + + if (retval == end()) + throw std::runtime_error("item is not from this container"); + + for (MWWorld::ContainerStoreIterator iter (begin()); iter != end(); ++iter) + { + if (stacks(*iter, item)) + { + iter->getRefData().setCount(iter->getRefData().getCount() + item.getRefData().getCount()); + item.getRefData().setCount(0); + retval = iter; + break; + } + } + return retval; +} + bool MWWorld::ContainerStore::stacks(const Ptr& ptr1, const Ptr& ptr2) { const MWWorld::Class& cls1 = ptr1.getClass(); @@ -225,6 +255,7 @@ MWWorld::ContainerStoreIterator MWWorld::ContainerStore::add (const Ptr& itemPtr pos.pos[1] = 0; pos.pos[2] = 0; item.getCellRef().setPosition(pos); + item.getCellRef().unsetRefNum(); // destroy link to content file std::string script = item.getClass().getScript(item); if(script != "") @@ -241,7 +272,7 @@ MWWorld::ContainerStoreIterator MWWorld::ContainerStore::add (const Ptr& itemPtr item.mCell = actorPtr.getCell(); } - item.mContainerStore = 0; + item.mContainerStore = this; MWBase::Environment::get().getWorld()->getLocalScripts().add(script, item); @@ -670,7 +701,8 @@ void MWWorld::ContainerStore::readState (const ESM::InventoryState& state) for (std::vector >::const_iterator iter (state.mLights.begin()); iter!=state.mLights.end(); ++iter) { - getState (lights, iter->first); + int slot = iter->second; + setSlot (getState (lights, iter->first), slot); } mLevelledItemMap = state.mLevelledItemMap; diff --git a/apps/openmw/mwworld/containerstore.hpp b/apps/openmw/mwworld/containerstore.hpp index 7c81bdb6e..6d9d7a6bb 100644 --- a/apps/openmw/mwworld/containerstore.hpp +++ b/apps/openmw/mwworld/containerstore.hpp @@ -134,6 +134,11 @@ namespace MWWorld void unstack (const Ptr& ptr, const Ptr& container); ///< Unstack an item in this container. The item's count will be set to 1, then a new stack will be added with (origCount-1). + MWWorld::ContainerStoreIterator restack (const MWWorld::Ptr& item); + ///< Attempt to re-stack an item in this container. + /// If a compatible stack is found, the item's count is added to that stack, then the original is deleted. + /// @return If the item was stacked, return the stack, otherwise return the old (untouched) item. + /// @return How many items with refID \a id are in this container? int count (const std::string& id); diff --git a/apps/openmw/mwworld/esmstore.cpp b/apps/openmw/mwworld/esmstore.cpp index 03d928d2a..56cb05c64 100644 --- a/apps/openmw/mwworld/esmstore.cpp +++ b/apps/openmw/mwworld/esmstore.cpp @@ -68,13 +68,12 @@ void ESMStore::load(ESM::ESMReader &esm, Loading::Listener* listener) if (it == mStores.end()) { if (n.val == ESM::REC_INFO) { - std::string id = esm.getHNOString("INAM"); - if (dialogue) { - ESM::DialInfo info; - info.mId = id; - info.load(esm); - dialogue->addInfo(info, esm.getIndex() != 0); - } else { + if (dialogue) + { + dialogue->readInfo(esm, esm.getIndex() != 0); + } + else + { std::cerr << "error: info record without dialog" << std::endl; esm.skipRecord(); } @@ -82,7 +81,13 @@ void ESMStore::load(ESM::ESMReader &esm, Loading::Listener* listener) mMagicEffects.load (esm); } else if (n.val == ESM::REC_SKIL) { mSkills.load (esm); - } else { + } + else if (n.val==ESM::REC_FILT || ESM::REC_DBGP) + { + // ignore project file only records + esm.skipRecord(); + } + else { std::stringstream error; error << "Unknown record: " << n.toString(); throw std::runtime_error(error.str()); @@ -100,6 +105,13 @@ void ESMStore::load(ESM::ESMReader &esm, Loading::Listener* listener) } it->second->load(esm, id); + // DELE can also occur after the usual subrecords + if (esm.isNextSub("DELE")) { + esm.skipRecord(); + it->second->eraseStatic(id); + continue; + } + if (n.val==ESM::REC_DIAL) { dialogue = const_cast(mDialogs.find(id)); } else { @@ -123,6 +135,7 @@ void ESMStore::setUp() mSkills.setUp(); mMagicEffects.setUp(); mAttributes.setUp(); + mDialogs.setUp(); } int ESMStore::countSavedGameRecords() const diff --git a/apps/openmw/mwworld/fallback.cpp b/apps/openmw/mwworld/fallback.cpp index 569a6b50c..c0b21b2ef 100644 --- a/apps/openmw/mwworld/fallback.cpp +++ b/apps/openmw/mwworld/fallback.cpp @@ -41,8 +41,9 @@ namespace MWWorld unsigned int j=0; for(unsigned int i=0;i(ret[0])/255.f,boost::lexical_cast(ret[1])/255.f,boost::lexical_cast(ret[2])/255.f); } } diff --git a/apps/openmw/mwworld/globals.cpp b/apps/openmw/mwworld/globals.cpp index 663af640b..15ba27498 100644 --- a/apps/openmw/mwworld/globals.cpp +++ b/apps/openmw/mwworld/globals.cpp @@ -100,7 +100,7 @@ namespace MWWorld if (iter!=mVariables.end()) iter->second.read (reader, ESM::Variant::Format_Global); else - reader.skipHRecord(); + reader.skipRecord(); return true; } diff --git a/apps/openmw/mwworld/inventorystore.cpp b/apps/openmw/mwworld/inventorystore.cpp index 2eb8aeb46..9032b04e1 100644 --- a/apps/openmw/mwworld/inventorystore.cpp +++ b/apps/openmw/mwworld/inventorystore.cpp @@ -68,20 +68,21 @@ MWWorld::InventoryStore::InventoryStore() , mUpdatesEnabled (true) , mFirstAutoEquip(true) , mListener(NULL) + , mRechargingItemsUpToDate(false) { initSlots (mSlots); } MWWorld::InventoryStore::InventoryStore (const InventoryStore& store) -: ContainerStore (store) + : ContainerStore (store) , mSelectedEnchantItem(end()) + , mMagicEffects(store.mMagicEffects) + , mFirstAutoEquip(store.mFirstAutoEquip) + , mListener(store.mListener) + , mUpdatesEnabled(store.mUpdatesEnabled) + , mPermanentMagicEffectMagnitudes(store.mPermanentMagicEffectMagnitudes) + , mRechargingItemsUpToDate(false) { - mMagicEffects = store.mMagicEffects; - mFirstAutoEquip = store.mFirstAutoEquip; - mListener = store.mListener; - mUpdatesEnabled = store.mUpdatesEnabled; - - mPermanentMagicEffectMagnitudes = store.mPermanentMagicEffectMagnitudes; copySlots (store); } @@ -91,6 +92,7 @@ MWWorld::InventoryStore& MWWorld::InventoryStore::operator= (const InventoryStor mMagicEffects = store.mMagicEffects; mFirstAutoEquip = store.mFirstAutoEquip; mPermanentMagicEffectMagnitudes = store.mPermanentMagicEffectMagnitudes; + mRechargingItemsUpToDate = false; ContainerStore::operator= (store); mSlots.clear(); copySlots (store); @@ -111,8 +113,6 @@ MWWorld::ContainerStoreIterator MWWorld::InventoryStore::add(const Ptr& itemPtr, autoEquip(actorPtr); } - updateRechargingItems(); - return retVal; } @@ -148,16 +148,18 @@ void MWWorld::InventoryStore::equip (int slot, const ContainerStoreIterator& ite flagAsModified(); fireEquipmentChangedEvent(); + updateMagicEffects(actor); } void MWWorld::InventoryStore::unequipAll(const MWWorld::Ptr& actor) { - // Only *one* change event should be fired mUpdatesEnabled = false; for (int slot=0; slot < MWWorld::InventoryStore::Slots; ++slot) unequipSlot(slot, actor); + mUpdatesEnabled = true; + fireEquipmentChangedEvent(); updateMagicEffects(actor); } @@ -198,19 +200,15 @@ void MWWorld::InventoryStore::autoEquip (const MWWorld::Ptr& actor) continue; } - // Don't auto-equip probes or lockpicks. NPCs can't use them (yet). And AiCombat would attempt to "attack" with them. - // NOTE: In the future AiCombat should handle equipping appropriate weapons - if (test.getTypeName() == typeid(ESM::Lockpick).name() || test.getTypeName() == typeid(ESM::Probe).name()) - continue; - // Only autoEquip if we are the original owner of the item. // This stops merchants from auto equipping anything you sell to them. // ...unless this is a companion, he should always equip items given to him. if (!Misc::StringUtils::ciEqual(test.getCellRef().getOwner(), actor.getCellRef().getRefId()) && (actor.getClass().getScript(actor).empty() || !actor.getRefData().getLocals().getIntVar(actor.getClass().getScript(actor), "companion"))) + { continue; - + } int testSkill = test.getClass().getEquipmentSkill (test); std::pair, bool> itemsSlots = @@ -219,30 +217,27 @@ void MWWorld::InventoryStore::autoEquip (const MWWorld::Ptr& actor) for (std::vector::const_iterator iter2 (itemsSlots.first.begin()); iter2!=itemsSlots.first.end(); ++iter2) { - bool use = false; + if (*iter2 == Slot_CarriedRight) // Items in right hand are situational use, so don't equip them. + // Equipping weapons is handled by AiCombat. Anything else (lockpicks, probes) can't be used by NPCs anyway (yet) + continue; - if (slots_.at (*iter2)==end()) - use = true; // slot was empty before -> skip all further checks - else + if (slots_.at (*iter2)!=end()) { Ptr old = *slots_.at (*iter2); - if (!use) + // check skill + int oldSkill = old.getClass().getEquipmentSkill (old); + + bool use = false; + if (testSkill!=-1 && oldSkill==-1) + use = true; + else if (testSkill!=-1 && oldSkill!=-1 && testSkill!=oldSkill) { - // check skill - int oldSkill = - old.getClass().getEquipmentSkill (old); + if (actor.getClass().getSkill(actor, oldSkill) > actor.getClass().getSkill (actor, testSkill)) + continue; // rejected, because old item better matched the NPC's skills. - if (testSkill!=-1 && oldSkill==-1) + if (actor.getClass().getSkill(actor, oldSkill) < actor.getClass().getSkill (actor, testSkill)) use = true; - else if (testSkill!=-1 && oldSkill!=-1 && testSkill!=oldSkill) - { - if (actor.getClass().getSkill(actor, oldSkill) > actor.getClass().getSkill (actor, testSkill)) - continue; // rejected, because old item better matched the NPC's skills. - - if (actor.getClass().getSkill(actor, oldSkill) < actor.getClass().getSkill (actor, testSkill)) - use = true; - } } if (!use) @@ -253,8 +248,6 @@ void MWWorld::InventoryStore::autoEquip (const MWWorld::Ptr& actor) { continue; } - - use = true; } } @@ -287,11 +280,13 @@ void MWWorld::InventoryStore::autoEquip (const MWWorld::Ptr& actor) bool changed = false; for (std::size_t i=0; igetRefData().getCount()) { - if (stacks(*iter, *it)) + retval = restack(*it); + + if (actor.getRefData().getHandle() == "player") { - iter->getRefData().setCount(iter->getRefData().getCount() + it->getRefData().getCount()); - it->getRefData().setCount(0); - retval = iter; - break; + // Unset OnPCEquip Variable on item's script, if it has a script with that variable declared + const std::string& script = it->getClass().getScript(*it); + if (script != "") + (*it).getRefData().getLocals().setVarByInt(script, "onpcequip", 0); } - } - - if (actor.getRefData().getHandle() == "player") - { - // Unset OnPCEquip Variable on item's script, if it has a script with that variable declared - const std::string& script = it->getClass().getScript(*it); - if (script != "") - (*it).getRefData().getLocals().setVarByInt(script, "onpcequip", 0); if ((mSelectedEnchantItem != end()) && (mSelectedEnchantItem == it)) { @@ -624,6 +610,11 @@ void MWWorld::InventoryStore::updateRechargingItems() void MWWorld::InventoryStore::rechargeItems(float duration) { + if (!mRechargingItemsUpToDate) + { + updateRechargingItems(); + mRechargingItemsUpToDate = true; + } for (TRechargingItems::iterator it = mRechargingItems.begin(); it != mRechargingItems.end(); ++it) { if (it->first->getCellRef().getEnchantmentCharge() == -1 @@ -633,14 +624,21 @@ void MWWorld::InventoryStore::rechargeItems(float duration) static float fMagicItemRechargePerSecond = MWBase::Environment::get().getWorld()->getStore().get().find( "fMagicItemRechargePerSecond")->getFloat(); - it->first->getCellRef().setEnchantmentCharge(std::min (it->first->getCellRef().getEnchantmentCharge() + fMagicItemRechargePerSecond * duration, - it->second)); + if (it->first->getCellRef().getEnchantmentCharge() <= it->second) + { + it->first->getCellRef().setEnchantmentCharge(std::min (it->first->getCellRef().getEnchantmentCharge() + fMagicItemRechargePerSecond * duration, + it->second)); + + // attempt to restack when fully recharged + if (it->first->getCellRef().getEnchantmentCharge() == it->second) + it->first = restack(*it->first); + } } } void MWWorld::InventoryStore::purgeEffect(short effectId) { - mMagicEffects.add(MWMechanics::EffectKey(effectId), -mMagicEffects.get(MWMechanics::EffectKey(effectId)).mMagnitude); + mMagicEffects.remove(MWMechanics::EffectKey(effectId)); } void MWWorld::InventoryStore::clear() diff --git a/apps/openmw/mwworld/inventorystore.hpp b/apps/openmw/mwworld/inventorystore.hpp index 95b956907..16d965cda 100644 --- a/apps/openmw/mwworld/inventorystore.hpp +++ b/apps/openmw/mwworld/inventorystore.hpp @@ -102,6 +102,8 @@ namespace MWWorld typedef std::vector > TRechargingItems; TRechargingItems mRechargingItems; + bool mRechargingItemsUpToDate; + void copySlots (const InventoryStore& store); void initSlots (TSlots& slots_); @@ -141,7 +143,7 @@ namespace MWWorld /// @return if stacking happened, return iterator to the item that was stacked against, otherwise iterator to the newly inserted item. void equip (int slot, const ContainerStoreIterator& iterator, const Ptr& actor); - ///< \note \a iterator can be an end-iterator + ///< \warning \a iterator can not be an end()-iterator, use unequip function instead void setSelectedEnchantItem(const ContainerStoreIterator& iterator); ///< set the selected magic item (for using enchantments of type "Cast once" or "Cast when used") diff --git a/apps/openmw/mwworld/livecellref.cpp b/apps/openmw/mwworld/livecellref.cpp index 0921d3a1b..dd313632b 100644 --- a/apps/openmw/mwworld/livecellref.cpp +++ b/apps/openmw/mwworld/livecellref.cpp @@ -30,8 +30,18 @@ void MWWorld::LiveCellRefBase::loadImp (const ESM::ObjectState& state) { if (const ESM::Script* script = MWBase::Environment::get().getWorld()->getStore().get().search (scriptId)) { - mData.setLocals (*script); - mData.getLocals().read (state.mLocals, scriptId); + try + { + mData.setLocals (*script); + mData.getLocals().read (state.mLocals, scriptId); + } + catch (const std::exception& exception) + { + std::cerr + << "failed to load state for local script " << scriptId + << " because an exception has been thrown: " << exception.what() + << std::endl; + } } } } diff --git a/apps/openmw/mwworld/localscripts.cpp b/apps/openmw/mwworld/localscripts.cpp index 8a671cea8..f3a647124 100644 --- a/apps/openmw/mwworld/localscripts.cpp +++ b/apps/openmw/mwworld/localscripts.cpp @@ -93,9 +93,18 @@ void MWWorld::LocalScripts::add (const std::string& scriptName, const Ptr& ptr) { if (const ESM::Script *script = mStore.get().find (scriptName)) { - ptr.getRefData().setLocals (*script); + try + { + ptr.getRefData().setLocals (*script); - mScripts.push_back (std::make_pair (scriptName, ptr)); + mScripts.push_back (std::make_pair (scriptName, ptr)); + } + catch (const std::exception& exception) + { + std::cerr + << "failed to add local script " << scriptName + << " because an exception has been thrown: " << exception.what() << std::endl; + } } } diff --git a/apps/openmw/mwworld/manualref.hpp b/apps/openmw/mwworld/manualref.hpp index 3842e7ff1..0becd7524 100644 --- a/apps/openmw/mwworld/manualref.hpp +++ b/apps/openmw/mwworld/manualref.hpp @@ -28,7 +28,7 @@ namespace MWWorld cellRef.mRefNum.mContentFile = -1; cellRef.mRefID = name; cellRef.mScale = 1; - cellRef.mFactIndex = 0; + cellRef.mFactionRank = 0; cellRef.mCharge = -1; cellRef.mGoldValue = 1; cellRef.mEnchantmentCharge = -1; diff --git a/apps/openmw/mwworld/omwloader.cpp b/apps/openmw/mwworld/omwloader.cpp deleted file mode 100644 index 8562a4fe0..000000000 --- a/apps/openmw/mwworld/omwloader.cpp +++ /dev/null @@ -1,17 +0,0 @@ -#include "omwloader.hpp" - -namespace MWWorld -{ - -OmwLoader::OmwLoader(Loading::Listener& listener) - : ContentLoader(listener) -{ -} - -void OmwLoader::load(const boost::filesystem::path& filepath, int& index) -{ - ContentLoader::load(filepath.filename(), index); -} - -} /* namespace MWWorld */ - diff --git a/apps/openmw/mwworld/omwloader.hpp b/apps/openmw/mwworld/omwloader.hpp deleted file mode 100644 index cb9faa430..000000000 --- a/apps/openmw/mwworld/omwloader.hpp +++ /dev/null @@ -1,21 +0,0 @@ -#ifndef OMWLOADER_HPP -#define OMWLOADER_HPP - -#include "contentloader.hpp" - -namespace MWWorld -{ - -/** - * @brief Placeholder for real OpenMW content loader - */ -struct OmwLoader : public ContentLoader -{ - OmwLoader(Loading::Listener& listener); - - void load(const boost::filesystem::path& filepath, int& index); -}; - -} /* namespace MWWorld */ - -#endif /* OMWLOADER_HPP */ diff --git a/apps/openmw/mwworld/physicssystem.cpp b/apps/openmw/mwworld/physicssystem.cpp index e93d9e640..eecc4a02d 100644 --- a/apps/openmw/mwworld/physicssystem.cpp +++ b/apps/openmw/mwworld/physicssystem.cpp @@ -12,7 +12,9 @@ #include #include +#include #include +#include #include @@ -22,14 +24,71 @@ #include "../mwbase/environment.hpp" #include "../mwmechanics/creaturestats.hpp" +#include "../mwmechanics/movement.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/cellstore.hpp" +#include "../apps/openmw/mwrender/animation.hpp" +#include "../apps/openmw/mwbase/world.hpp" +#include "../apps/openmw/mwbase/environment.hpp" + #include "ptr.hpp" #include "class.hpp" using namespace Ogre; + +namespace +{ + +void animateCollisionShapes (std::map& map, btDynamicsWorld* dynamicsWorld) +{ + for (std::map::iterator it = map.begin(); + it != map.end(); ++it) + { + MWWorld::Ptr ptr = MWBase::Environment::get().getWorld()->searchPtrViaHandle(it->first->mName); + if (ptr.isEmpty()) // Shouldn't happen + throw std::runtime_error("can't find Ptr"); + + MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(ptr); + if (!animation) // Shouldn't happen either, since keyframe-controlled objects are not batched in StaticGeometry + throw std::runtime_error("can't find Animation for " + ptr.getCellRef().getRefId()); + + OEngine::Physic::AnimatedShapeInstance& instance = it->second; + + std::map& shapes = instance.mAnimatedShapes; + for (std::map::iterator shapeIt = shapes.begin(); + shapeIt != shapes.end(); ++shapeIt) + { + + Ogre::Node* bone; + if (shapeIt->first.empty()) + // HACK: see NifSkeletonLoader::buildBones + bone = animation->getNode(" "); + else + bone = animation->getNode(shapeIt->first); + + if (bone == NULL) + throw std::runtime_error("can't find bone"); + + btCompoundShape* compound = dynamic_cast(instance.mCompound); + + btTransform trans; + trans.setOrigin(BtOgre::Convert::toBullet(bone->_getDerivedPosition())); + trans.setRotation(BtOgre::Convert::toBullet(bone->_getDerivedOrientation())); + + compound->getChildShape(shapeIt->second)->setLocalScaling(BtOgre::Convert::toBullet(bone->_getDerivedScale())); + compound->updateChildTransform(shapeIt->second, trans); + } + + // needed because we used btDynamicsWorld::setForceUpdateAllAabbs(false) + dynamicsWorld->updateSingleAabb(it->first); + } +} + +} + + namespace MWWorld { @@ -164,7 +223,7 @@ namespace MWWorld public: - static Ogre::Vector3 traceDown(const MWWorld::Ptr &ptr, OEngine::Physic::PhysicEngine *engine) + static Ogre::Vector3 traceDown(const MWWorld::Ptr &ptr, OEngine::Physic::PhysicEngine *engine, float maxHeight) { const ESM::Position &refpos = ptr.getRefData().getPosition(); Ogre::Vector3 position(refpos.pos); @@ -173,28 +232,57 @@ namespace MWWorld if (!physicActor) return position; - const int maxHeight = 200.f; OEngine::Physic::ActorTracer tracer; - tracer.findGround(physicActor->getCollisionBody(), position, position-Ogre::Vector3(0,0,maxHeight), engine); + tracer.findGround(physicActor, position, position-Ogre::Vector3(0,0,maxHeight), engine); if(tracer.mFraction >= 1.0f) { physicActor->setOnGround(false); return position; } + else + { + // Check if we actually found a valid spawn point (use an infinitely thin ray this time). + // Required for some broken door destinations in Morrowind.esm, where the spawn point + // intersects with other geometry if the actor's base is taken into account + btVector3 from = BtOgre::Convert::toBullet(position); + btVector3 to = from - btVector3(0,0,maxHeight); + + btCollisionWorld::ClosestRayResultCallback resultCallback1(from, to); + resultCallback1.m_collisionFilterGroup = 0xff; + resultCallback1.m_collisionFilterMask = OEngine::Physic::CollisionType_World|OEngine::Physic::CollisionType_HeightMap; + + engine->mDynamicsWorld->rayTest(from, to, resultCallback1); + if (resultCallback1.hasHit() && + (BtOgre::Convert::toOgre(resultCallback1.m_hitPointWorld).distance(tracer.mEndPos) > 30 + || getSlope(tracer.mPlaneNormal) > sMaxSlope)) + { + physicActor->setOnGround(getSlope(BtOgre::Convert::toOgre(resultCallback1.m_hitNormalWorld)) <= sMaxSlope); + return BtOgre::Convert::toOgre(resultCallback1.m_hitPointWorld) + Ogre::Vector3(0,0,1.f); + } - physicActor->setOnGround(getSlope(tracer.mPlaneNormal) <= sMaxSlope); + physicActor->setOnGround(getSlope(tracer.mPlaneNormal) <= sMaxSlope); - return tracer.mEndPos; + return tracer.mEndPos; + } } static Ogre::Vector3 move(const MWWorld::Ptr &ptr, const Ogre::Vector3 &movement, float time, - bool isFlying, float waterlevel, float slowFall, OEngine::Physic::PhysicEngine *engine) + bool isFlying, float waterlevel, float slowFall, OEngine::Physic::PhysicEngine *engine + , std::map& collisionTracker + , std::map& standingCollisionTracker) { const ESM::Position &refpos = ptr.getRefData().getPosition(); Ogre::Vector3 position(refpos.pos); - /* Anything to collide with? */ + // Early-out for totally static creatures + // (Not sure if gravity should still apply?) + if (!ptr.getClass().canWalk(ptr) && !ptr.getClass().canFly(ptr) && !ptr.getClass().canSwim(ptr)) + return position; + OEngine::Physic::PhysicActor *physicActor = engine->getCharacter(ptr.getRefData().getHandle()); + // Reset per-frame data + physicActor->setWalkingOnWater(false); + /* Anything to collide with? */ if(!physicActor || !physicActor->getCollisionMode()) { return position + (Ogre::Quaternion(Ogre::Radian(refpos.rot[2]), Ogre::Vector3::NEGATIVE_UNIT_Z) * @@ -222,8 +310,6 @@ namespace MWWorld */ OEngine::Physic::ActorTracer tracer; - bool wasOnGround = false; - bool isOnGround = false; Ogre::Vector3 inertia(0.0f); Ogre::Vector3 velocity; @@ -236,38 +322,37 @@ namespace MWWorld else { velocity = Ogre::Quaternion(Ogre::Radian(refpos.rot[2]), Ogre::Vector3::NEGATIVE_UNIT_Z) * movement; + // not in water nor can fly, so need to deal with gravity if(!physicActor->getOnGround()) // if current OnGround status is false, must be falling or jumping { - // If falling, add part of the incoming velocity with the current inertia - // TODO: but we could be jumping up? - velocity = velocity * time + physicActor->getInertialForce(); - - // avoid getting infinite inertia in air + // If falling or jumping up, add part of the incoming velocity with the current inertia, + // but don't allow increasing inertia beyond actor's speed (except on the initial jump impulse) float actorSpeed = ptr.getClass().getSpeed(ptr); - float speedXY = Ogre::Vector2(velocity.x, velocity.y).length(); - if (speedXY > actorSpeed) + float cap = std::max(actorSpeed, Ogre::Vector2(physicActor->getInertialForce().x, physicActor->getInertialForce().y).length()); + Ogre::Vector3 newVelocity = velocity + physicActor->getInertialForce(); + if (Ogre::Vector2(newVelocity.x, newVelocity.y).squaredLength() > cap*cap) { - velocity.x *= actorSpeed / speedXY; - velocity.y *= actorSpeed / speedXY; + velocity = newVelocity; + float speedXY = Ogre::Vector2(velocity.x, velocity.y).length(); + velocity.x *= cap / speedXY; + velocity.y *= cap / speedXY; } + else + velocity = newVelocity; } inertia = velocity; // NOTE: velocity is for z axis only in this code block - - if(!(movement.z > 0.0f)) // falling or moving horizontally (or stationary?) check if we're on ground now - { - wasOnGround = physicActor->getOnGround(); // store current state - tracer.doTrace(colobj, position, position - Ogre::Vector3(0,0,2), engine); // check if down 2 possible - if(tracer.mFraction < 1.0f && getSlope(tracer.mPlaneNormal) <= sMaxSlope) - isOnGround = true; - } } + ptr.getClass().getMovementSettings(ptr).mPosition[2] = 0; - // NOTE: isOnGround was initialised false, so should stay false if falling or sliding horizontally - if(isOnGround) + // Now that we have the effective movement vector, apply wind forces to it + if (MWBase::Environment::get().getWorld()->isInStorm()) { - // if we're on the ground, don't try to fall any more - velocity.z = std::max(0.0f, velocity.z); // NOTE: two different velocity assignments above + Ogre::Vector3 stormDirection = MWBase::Environment::get().getWorld()->getStormDirection(); + Ogre::Degree angle = stormDirection.angleBetween(velocity); + static const float fStromWalkMult = MWBase::Environment::get().getWorld()->getStore().get() + .find("fStromWalkMult")->getFloat(); + velocity *= 1.f-(fStromWalkMult * (angle.valueDegrees()/180.f)); } Ogre::Vector3 newPosition = position; @@ -299,17 +384,39 @@ namespace MWWorld continue; // velocity updated, calculate nextpos again } - // trace to where character would go if there were no obstructions - tracer.doTrace(colobj, newPosition, nextpos, engine); + if(newPosition.squaredDistance(nextpos) > 0.0001) + { + // trace to where character would go if there were no obstructions + tracer.doTrace(colobj, newPosition, nextpos, engine); - // check for obstructions - if(tracer.mFraction >= 1.0f) + // check for obstructions + if(tracer.mFraction >= 1.0f) + { + newPosition = tracer.mEndPos; // ok to move, so set newPosition + break; + } + else + { + const btCollisionObject* standingOn = tracer.mHitObject; + if (const OEngine::Physic::RigidBody* body = dynamic_cast(standingOn)) + { + collisionTracker[ptr.getRefData().getHandle()] = body->mName; + } + } + } + else { - newPosition = tracer.mEndPos; // ok to move, so set newPosition - remainingTime *= (1.0f-tracer.mFraction); // FIXME: remainingTime is no longer used so don't set it? + // The current position and next position are nearly the same, so just exit. + // Note: Bullet can trigger an assert in debug modes if the positions + // are the same, since that causes it to attempt to normalize a zero + // length vector (which can also happen with nearly identical vectors, since + // precision can be lost due to any math Bullet does internally). Since we + // aren't performing any collision detection, we want to reject the next + // position, so that we don't slowly move inside another object. break; } + Ogre::Vector3 oldPosition = newPosition; // We hit something. Try to step up onto it. (NOTE: stepMove does not allow stepping over) // NOTE: stepMove modifies newPosition if successful @@ -319,8 +426,6 @@ namespace MWWorld if((ptr.getClass().canSwim(ptr) && !ptr.getClass().canWalk(ptr)) && newPosition.z > (waterlevel - halfExtents.z * 0.5)) newPosition = oldPosition; - else // Only on the ground if there's gravity - isOnGround = !(newPosition.z < waterlevel || isFlying); } else { @@ -337,12 +442,26 @@ namespace MWWorld } } - if(isOnGround || wasOnGround) + bool isOnGround = false; + if (!(inertia.z > 0.f) && !(newPosition.z < waterlevel)) { - tracer.doTrace(colobj, newPosition, newPosition - Ogre::Vector3(0,0,sStepSize+2.0f), engine); + Ogre::Vector3 from = newPosition; + Ogre::Vector3 to = newPosition - (physicActor->getOnGround() ? + Ogre::Vector3(0,0,sStepSize+2.f) : Ogre::Vector3(0,0,2.f)); + tracer.doTrace(colobj, from, to, engine); if(tracer.mFraction < 1.0f && getSlope(tracer.mPlaneNormal) <= sMaxSlope) { - newPosition.z = tracer.mEndPos.z + 1.0f; + const btCollisionObject* standingOn = tracer.mHitObject; + if (const OEngine::Physic::RigidBody* body = dynamic_cast(standingOn)) + { + standingCollisionTracker[ptr.getRefData().getHandle()] = body->mName; + } + if (standingOn->getBroadphaseHandle()->m_collisionFilterGroup == OEngine::Physic::CollisionType_Water) + physicActor->setWalkingOnWater(true); + + if (!isFlying) + newPosition.z = tracer.mEndPos.z + 1.0f; + isOnGround = true; } else @@ -353,10 +472,9 @@ namespace MWWorld physicActor->setInertialForce(Ogre::Vector3(0.0f)); else { - float diff = time*-627.2f; + inertia.z += time * -627.2f; if (inertia.z < 0) - diff *= slowFall; - inertia.z += diff; + inertia.z *= slowFall; physicActor->setInertialForce(inertia); } physicActor->setOnGround(isOnGround); @@ -368,7 +486,7 @@ namespace MWWorld PhysicsSystem::PhysicsSystem(OEngine::Render::OgreRenderer &_rend) : - mRender(_rend), mEngine(0), mTimeAccum(0.0f) + mRender(_rend), mEngine(0), mTimeAccum(0.0f), mWaterEnabled(false), mWaterHeight(0) { // Create physics. shapeLoader is deleted by the physic engine NifBullet::ManualBulletShapeLoader* shapeLoader = new NifBullet::ManualBulletShapeLoader(); @@ -377,7 +495,10 @@ namespace MWWorld PhysicsSystem::~PhysicsSystem() { + if (mWaterCollisionObject.get()) + mEngine->mDynamicsWorld->removeCollisionObject(mWaterCollisionObject.get()); delete mEngine; + delete OEngine::Physic::BulletShapeManager::getSingletonPtr(); } OEngine::Physic::PhysicEngine* PhysicsSystem::getEngine() @@ -492,7 +613,7 @@ namespace MWWorld return std::make_pair(true, ray.getPoint(len * test.second)); } - std::pair PhysicsSystem::castRay(float mouseX, float mouseY, Ogre::Vector3* normal) + std::pair PhysicsSystem::castRay(float mouseX, float mouseY, Ogre::Vector3* normal, std::string* hit) { Ogre::Ray ray = mRender.getCamera()->getCameraToViewportRay( mouseX, @@ -510,18 +631,20 @@ namespace MWWorld return std::make_pair(false, Ogre::Vector3()); else { + if (hit != NULL) + *hit = result.first; return std::make_pair(true, ray.getPoint(200*result.second)); /// \todo make this distance (ray length) configurable } } - std::vector PhysicsSystem::getCollisions(const Ptr &ptr) + std::vector PhysicsSystem::getCollisions(const Ptr &ptr, int collisionGroup, int collisionMask) { - return mEngine->getCollisions(ptr.getRefData().getBaseNode()->getName()); + return mEngine->getCollisions(ptr.getRefData().getBaseNode()->getName(), collisionGroup, collisionMask); } - Ogre::Vector3 PhysicsSystem::traceDown(const MWWorld::Ptr &ptr) + Ogre::Vector3 PhysicsSystem::traceDown(const MWWorld::Ptr &ptr, float maxHeight) { - return MovementSolver::traceDown(ptr, mEngine); + return MovementSolver::traceDown(ptr, mEngine, maxHeight); } void PhysicsSystem::addHeightField (float* heights, @@ -541,11 +664,10 @@ namespace MWWorld std::string mesh = ptr.getClass().getModel(ptr); Ogre::SceneNode* node = ptr.getRefData().getBaseNode(); handleToMesh[node->getName()] = mesh; - OEngine::Physic::RigidBody* body = mEngine->createAndAdjustRigidBody( - mesh, node->getName(), node->getScale().x, node->getPosition(), node->getOrientation(), 0, 0, false, placeable); - OEngine::Physic::RigidBody* raycastingBody = mEngine->createAndAdjustRigidBody( - mesh, node->getName(), node->getScale().x, node->getPosition(), node->getOrientation(), 0, 0, true, placeable); - mEngine->addRigidBody(body, true, raycastingBody); + mEngine->createAndAdjustRigidBody( + mesh, node->getName(), ptr.getCellRef().getScale(), node->getPosition(), node->getOrientation(), 0, 0, false, placeable); + mEngine->createAndAdjustRigidBody( + mesh, node->getName(), ptr.getCellRef().getScale(), node->getPosition(), node->getOrientation(), 0, 0, true, placeable); } void PhysicsSystem::addActor (const Ptr& ptr) @@ -570,11 +692,18 @@ namespace MWWorld const Ogre::Vector3 &position = node->getPosition(); if(OEngine::Physic::RigidBody *body = mEngine->getRigidBody(handle)) + { body->getWorldTransform().setOrigin(btVector3(position.x,position.y,position.z)); + mEngine->mDynamicsWorld->updateSingleAabb(body); + } if(OEngine::Physic::RigidBody *body = mEngine->getRigidBody(handle, true)) + { body->getWorldTransform().setOrigin(btVector3(position.x,position.y,position.z)); + mEngine->mDynamicsWorld->updateSingleAabb(body); + } + // Actors update their AABBs every frame (DISABLE_DEACTIVATION), so no need to do it manually if(OEngine::Physic::PhysicActor *physact = mEngine->getCharacter(handle)) physact->setPosition(position); } @@ -584,9 +713,10 @@ namespace MWWorld Ogre::SceneNode* node = ptr.getRefData().getBaseNode(); const std::string &handle = node->getName(); const Ogre::Quaternion &rotation = node->getOrientation(); + + // TODO: map to MWWorld::Ptr for faster access if (OEngine::Physic::PhysicActor* act = mEngine->getCharacter(handle)) { - //Needs to be changed act->setRotation(rotation); } if (OEngine::Physic::RigidBody* body = mEngine->getRigidBody(handle)) @@ -595,6 +725,7 @@ namespace MWWorld body->getWorldTransform().setRotation(btQuaternion(rotation.x, rotation.y, rotation.z, rotation.w)); else mEngine->boxAdjustExternal(handleToMesh[handle], body, node->getScale().x, node->getPosition(), rotation); + mEngine->mDynamicsWorld->updateSingleAabb(body); } if (OEngine::Physic::RigidBody* body = mEngine->getRigidBody(handle, true)) { @@ -602,6 +733,7 @@ namespace MWWorld body->getWorldTransform().setRotation(btQuaternion(rotation.x, rotation.y, rotation.z, rotation.w)); else mEngine->boxAdjustExternal(handleToMesh[handle], body, node->getScale().x, node->getPosition(), rotation); + mEngine->mDynamicsWorld->updateSingleAabb(body); } } @@ -621,7 +753,10 @@ namespace MWWorld } if (OEngine::Physic::PhysicActor* act = mEngine->getCharacter(handle)) - act->setScale(node->getScale().x); + { + // NOTE: Ignoring Npc::adjustScale (race height) on purpose. This is a bug in MW and must be replicated for compatibility reasons + act->setScale(ptr.getCellRef().getScale()); + } } bool PhysicsSystem::toggleCollisionMode() @@ -686,6 +821,13 @@ namespace MWWorld mMovementQueue.push_back(std::make_pair(ptr, movement)); } + void PhysicsSystem::clearQueuedMovement() + { + mMovementQueue.clear(); + mCollisions.clear(); + mStandingCollisions.clear(); + } + const PtrVelocityList& PhysicsSystem::applyQueuedMovement(float dt) { mMovementResults.clear(); @@ -693,6 +835,10 @@ namespace MWWorld mTimeAccum += dt; if(mTimeAccum >= 1.0f/60.0f) { + // Collision events should be available on every frame + mCollisions.clear(); + mStandingCollisions.clear(); + const MWBase::World *world = MWBase::Environment::get().getWorld(); PtrVelocityList::iterator iter = mMovementQueue.begin(); for(;iter != mMovementQueue.end();++iter) @@ -707,28 +853,23 @@ namespace MWWorld const MWMechanics::MagicEffects& effects = iter->first.getClass().getCreatureStats(iter->first).getMagicEffects(); bool waterCollision = false; - if (effects.get(ESM::MagicEffect::WaterWalking).mMagnitude + if (effects.get(ESM::MagicEffect::WaterWalking).getMagnitude() && cell->hasWater() && !world->isUnderwater(iter->first.getCell(), Ogre::Vector3(iter->first.getRefData().getPosition().pos))) waterCollision = true; - btStaticPlaneShape planeShape(btVector3(0,0,1), waterlevel); - btCollisionObject object; - object.setCollisionShape(&planeShape); - - if (waterCollision) - mEngine->dynamicsWorld->addCollisionObject(&object); + OEngine::Physic::PhysicActor *physicActor = mEngine->getCharacter(iter->first.getRefData().getHandle()); + if (!physicActor) // actor was already removed from the scene + continue; + physicActor->setCanWaterWalk(waterCollision); - // 100 points of slowfall reduce gravity by 90% (this is just a guess) - float slowFall = 1-std::min(std::max(0.f, (effects.get(ESM::MagicEffect::SlowFall).mMagnitude / 100.f) * 0.9f), 0.9f); + // Slow fall reduces fall speed by a factor of (effect magnitude / 200) + float slowFall = 1.f - std::max(0.f, std::min(1.f, effects.get(ESM::MagicEffect::SlowFall).getMagnitude() * 0.005f)); Ogre::Vector3 newpos = MovementSolver::move(iter->first, iter->second, mTimeAccum, world->isFlying(iter->first), - waterlevel, slowFall, mEngine); - - if (waterCollision) - mEngine->dynamicsWorld->removeCollisionObject(&object); + waterlevel, slowFall, mEngine, mCollisions, mStandingCollisions); float heightDiff = newpos.z - oldHeight; @@ -744,4 +885,109 @@ namespace MWWorld return mMovementResults; } + + void PhysicsSystem::stepSimulation(float dt) + { + animateCollisionShapes(mEngine->mAnimatedShapes, mEngine->mDynamicsWorld); + animateCollisionShapes(mEngine->mAnimatedRaycastingShapes, mEngine->mDynamicsWorld); + + mEngine->stepSimulation(dt); + } + + bool PhysicsSystem::isActorStandingOn(const Ptr &actor, const Ptr &object) const + { + const std::string& actorHandle = actor.getRefData().getHandle(); + const std::string& objectHandle = object.getRefData().getHandle(); + + for (std::map::const_iterator it = mStandingCollisions.begin(); + it != mStandingCollisions.end(); ++it) + { + if (it->first == actorHandle && it->second == objectHandle) + return true; + } + return false; + } + + void PhysicsSystem::getActorsStandingOn(const Ptr &object, std::vector &out) const + { + const std::string& objectHandle = object.getRefData().getHandle(); + + for (std::map::const_iterator it = mStandingCollisions.begin(); + it != mStandingCollisions.end(); ++it) + { + if (it->second == objectHandle) + out.push_back(it->first); + } + } + + bool PhysicsSystem::isActorCollidingWith(const Ptr &actor, const Ptr &object) const + { + const std::string& actorHandle = actor.getRefData().getHandle(); + const std::string& objectHandle = object.getRefData().getHandle(); + + for (std::map::const_iterator it = mCollisions.begin(); + it != mCollisions.end(); ++it) + { + if (it->first == actorHandle && it->second == objectHandle) + return true; + } + return false; + } + + void PhysicsSystem::getActorsCollidingWith(const Ptr &object, std::vector &out) const + { + const std::string& objectHandle = object.getRefData().getHandle(); + + for (std::map::const_iterator it = mCollisions.begin(); + it != mCollisions.end(); ++it) + { + if (it->second == objectHandle) + out.push_back(it->first); + } + } + + void PhysicsSystem::disableWater() + { + if (mWaterEnabled) + { + mWaterEnabled = false; + updateWater(); + } + } + + void PhysicsSystem::enableWater(float height) + { + if (!mWaterEnabled || mWaterHeight != height) + { + mWaterEnabled = true; + mWaterHeight = height; + updateWater(); + } + } + + void PhysicsSystem::setWaterHeight(float height) + { + if (mWaterHeight != height) + { + mWaterHeight = height; + updateWater(); + } + } + + void PhysicsSystem::updateWater() + { + if (mWaterCollisionObject.get()) + { + mEngine->mDynamicsWorld->removeCollisionObject(mWaterCollisionObject.get()); + } + + if (!mWaterEnabled) + return; + + mWaterCollisionObject.reset(new btCollisionObject()); + mWaterCollisionShape.reset(new btStaticPlaneShape(btVector3(0,0,1), mWaterHeight)); + mWaterCollisionObject->setCollisionShape(mWaterCollisionShape.get()); + mEngine->mDynamicsWorld->addCollisionObject(mWaterCollisionObject.get(), OEngine::Physic::CollisionType_Water, + OEngine::Physic::CollisionType_Actor); + } } diff --git a/apps/openmw/mwworld/physicssystem.hpp b/apps/openmw/mwworld/physicssystem.hpp index 899d7144d..7dc8acaa1 100644 --- a/apps/openmw/mwworld/physicssystem.hpp +++ b/apps/openmw/mwworld/physicssystem.hpp @@ -1,6 +1,8 @@ #ifndef GAME_MWWORLD_PHYSICSSYSTEM_H #define GAME_MWWORLD_PHYSICSSYSTEM_H +#include + #include #include @@ -32,6 +34,10 @@ namespace MWWorld PhysicsSystem (OEngine::Render::OgreRenderer &_rend); ~PhysicsSystem (); + void enableWater(float height); + void setWaterHeight(float height); + void disableWater(); + void addObject (const MWWorld::Ptr& ptr, bool placeable=false); void addActor (const MWWorld::Ptr& ptr); @@ -53,8 +59,10 @@ namespace MWWorld bool toggleCollisionMode(); - std::vector getCollisions(const MWWorld::Ptr &ptr); ///< get handles this object collides with - Ogre::Vector3 traceDown(const MWWorld::Ptr &ptr); + void stepSimulation(float dt); + + std::vector getCollisions(const MWWorld::Ptr &ptr, int collisionGroup, int collisionMask); ///< get handles this object collides with + Ogre::Vector3 traceDown(const MWWorld::Ptr &ptr, float maxHeight); std::pair getFacedHandle(float queryDistance); std::pair getHitContact(const std::string &name, @@ -70,9 +78,10 @@ namespace MWWorld std::pair castRay(const Ogre::Vector3 &orig, const Ogre::Vector3 &dir, float len); - std::pair castRay(float mouseX, float mouseY, Ogre::Vector3* normal = NULL); + std::pair castRay(float mouseX, float mouseY, Ogre::Vector3* normal = NULL, std::string* hit = NULL); ///< cast ray from the mouse, return true if it hit something and the first result /// @param normal if non-NULL, the hit normal will be written there (if there is a hit) + /// @param hit if non-NULL, the string handle of the hit object will be written there (if there is a hit) OEngine::Physic::PhysicEngine* getEngine(); @@ -82,19 +91,53 @@ namespace MWWorld /// be overwritten. Valid until the next call to applyQueuedMovement. void queueObjectMovement(const Ptr &ptr, const Ogre::Vector3 &velocity); + /// Apply all queued movements, then clear the list. const PtrVelocityList& applyQueuedMovement(float dt); + /// Clear the queued movements list without applying. + void clearQueuedMovement(); + + /// Return true if \a actor has been standing on \a object in this frame + /// This will trigger whenever the object is directly below the actor. + /// It doesn't matter if the actor is stationary or moving. + bool isActorStandingOn(const MWWorld::Ptr& actor, const MWWorld::Ptr& object) const; + + /// Get the handle of all actors standing on \a object in this frame. + void getActorsStandingOn(const MWWorld::Ptr& object, std::vector& out) const; + + /// Return true if \a actor has collided with \a object in this frame. + /// This will detect running into objects, but will not detect climbing stairs, stepping up a small object, etc. + bool isActorCollidingWith(const MWWorld::Ptr& actor, const MWWorld::Ptr& object) const; + + /// Get the handle of all actors colliding with \a object in this frame. + void getActorsCollidingWith(const MWWorld::Ptr& object, std::vector& out) const; + private: + void updateWater(); + OEngine::Render::OgreRenderer &mRender; OEngine::Physic::PhysicEngine* mEngine; std::map handleToMesh; + // Tracks all movement collisions happening during a single frame. + // This will detect e.g. running against a vertical wall. It will not detect climbing up stairs, + // stepping up small objects, etc. + std::map mCollisions; + + std::map mStandingCollisions; + PtrVelocityList mMovementQueue; PtrVelocityList mMovementResults; float mTimeAccum; + float mWaterHeight; + float mWaterEnabled; + + std::auto_ptr mWaterCollisionObject; + std::auto_ptr mWaterCollisionShape; + PhysicsSystem (const PhysicsSystem&); PhysicsSystem& operator= (const PhysicsSystem&); }; diff --git a/apps/openmw/mwworld/player.cpp b/apps/openmw/mwworld/player.cpp index 9913b888b..b3996f756 100644 --- a/apps/openmw/mwworld/player.cpp +++ b/apps/openmw/mwworld/player.cpp @@ -44,8 +44,9 @@ namespace MWWorld cellRef.mRefID = "player"; mPlayer = LiveCellRef(cellRef, player); - float* playerPos = mPlayer.mData.getPosition().pos; - playerPos[0] = playerPos[1] = playerPos[2] = 0; + ESM::Position playerPos = mPlayer.mData.getPosition(); + playerPos.pos[0] = playerPos.pos[1] = playerPos.pos[2] = 0; + mPlayer.mData.setPosition(playerPos); } void Player::set(const ESM::NPC *player) @@ -243,9 +244,23 @@ namespace MWWorld mPlayer.load (player.mObject); + getPlayer().getClass().getCreatureStats(getPlayer()).getAiSequence().clear(); + MWBase::World& world = *MWBase::Environment::get().getWorld(); - mCellStore = world.getCell (player.mCellId); + try + { + mCellStore = world.getCell (player.mCellId); + } + catch (...) + { + // Cell no longer exists. Place the player in a default cell. + ESM::Position pos = mPlayer.mData.getPosition(); + MWBase::Environment::get().getWorld()->indexToPosition(0, 0, pos.pos[0], pos.pos[1], true); + pos.pos[2] = 0; + mPlayer.mData.setPosition(pos); + mCellStore = world.getExterior(0,0); + } if (!player.mBirthsign.empty() && !world.getStore().get().search (player.mBirthsign)) diff --git a/apps/openmw/mwworld/projectilemanager.cpp b/apps/openmw/mwworld/projectilemanager.cpp index 4e4f0b271..afda6fe60 100644 --- a/apps/openmw/mwworld/projectilemanager.cpp +++ b/apps/openmw/mwworld/projectilemanager.cpp @@ -20,6 +20,8 @@ #include "../mwmechanics/spellcasting.hpp" #include "../mwrender/effectmanager.hpp" +#include "../mwrender/animation.hpp" +#include "../mwrender/renderconst.hpp" #include "../mwsound/sound.hpp" @@ -41,6 +43,9 @@ namespace MWWorld if(state.mObject->mControllers[i].getSource().isNull()) state.mObject->mControllers[i].setSource(Ogre::SharedPtr (new MWRender::EffectAnimationTime())); } + + MWRender::Animation::setRenderProperties(state.mObject, MWRender::RV_Misc, + MWRender::RQG_Main, MWRender::RQG_Alpha, 0.f, false, NULL); } void ProjectileManager::update(NifOgre::ObjectScenePtr object, float duration) @@ -57,22 +62,35 @@ namespace MWWorld void ProjectileManager::launchMagicBolt(const std::string &model, const std::string &sound, const std::string &spellId, float speed, bool stack, - const ESM::EffectList &effects, const Ptr &actor, const std::string &sourceName) + const ESM::EffectList &effects, const Ptr &caster, const std::string &sourceName, + const Ogre::Vector3& fallbackDirection) { - // Spawn at 0.75 * ActorHeight - float height = mPhysEngine.getCharacter(actor.getRefData().getHandle())->getHalfExtents().z * 2 * 0.75; + float height = 0; + if (OEngine::Physic::PhysicActor* actor = mPhysEngine.getCharacter(caster.getRefData().getHandle())) + height = actor->getHalfExtents().z * 2 * 0.75; // Spawn at 0.75 * ActorHeight - Ogre::Vector3 pos(actor.getRefData().getPosition().pos); + Ogre::Vector3 pos(caster.getRefData().getPosition().pos); pos.z += height; - Ogre::Quaternion orient = Ogre::Quaternion(Ogre::Radian(actor.getRefData().getPosition().rot[2]), Ogre::Vector3::NEGATIVE_UNIT_Z) * - Ogre::Quaternion(Ogre::Radian(actor.getRefData().getPosition().rot[0]), Ogre::Vector3::NEGATIVE_UNIT_X); + if (MWBase::Environment::get().getWorld()->isUnderwater(caster.getCell(), pos)) // Underwater casting not possible + return; + + Ogre::Quaternion orient; + if (caster.getClass().isActor()) + orient = Ogre::Quaternion(Ogre::Radian(caster.getRefData().getPosition().rot[2]), Ogre::Vector3::NEGATIVE_UNIT_Z) * + Ogre::Quaternion(Ogre::Radian(caster.getRefData().getPosition().rot[0]), Ogre::Vector3::NEGATIVE_UNIT_X); + else + orient = Ogre::Vector3::UNIT_Y.getRotationTo(fallbackDirection); MagicBoltState state; state.mSourceName = sourceName; state.mId = model; state.mSpellId = spellId; - state.mActorId = actor.getClass().getCreatureStats(actor).getActorId(); + state.mCasterHandle = caster.getRefData().getHandle(); + if (caster.getClass().isActor()) + state.mActorId = caster.getClass().getCreatureStats(caster).getActorId(); + else + state.mActorId = -1; state.mSpeed = speed; state.mStack = stack; state.mSoundId = sound; @@ -135,7 +153,8 @@ namespace MWWorld Ogre::Vector3 pos(it->mNode->getPosition()); Ogre::Vector3 newPos = pos + direction * duration * speed; - it->mSound->setPosition(newPos); + if (it->mSound.get()) + it->mSound->setPosition(newPos); it->mNode->setPosition(newPos); @@ -145,14 +164,17 @@ namespace MWWorld // TODO: use a proper btRigidBody / btGhostObject? btVector3 from(pos.x, pos.y, pos.z); btVector3 to(newPos.x, newPos.y, newPos.z); - std::vector > collisions = mPhysEngine.rayTest2(from, to); + + std::vector > collisions = mPhysEngine.rayTest2(from, to, OEngine::Physic::CollisionType_Projectile); bool hit=false; for (std::vector >::iterator cIt = collisions.begin(); cIt != collisions.end() && !hit; ++cIt) { MWWorld::Ptr obstacle = MWBase::Environment::get().getWorld()->searchPtrViaHandle(cIt->second); - MWWorld::Ptr caster = MWBase::Environment::get().getWorld()->searchPtrViaActorId(it->mActorId); + MWWorld::Ptr caster = MWBase::Environment::get().getWorld()->searchPtrViaHandle(it->mCasterHandle); + if (caster.isEmpty()) + caster = MWBase::Environment::get().getWorld()->searchPtrViaActorId(it->mActorId); if (!obstacle.isEmpty() && obstacle == caster) continue; @@ -176,6 +198,11 @@ namespace MWWorld hit = true; } + + // Explodes when hitting water + if (MWBase::Environment::get().getWorld()->isUnderwater(MWBase::Environment::get().getWorld()->getPlayerPtr().getCell(), newPos)) + hit = true; + if (hit) { MWWorld::Ptr caster = MWBase::Environment::get().getWorld()->searchPtrViaActorId(it->mActorId); @@ -214,7 +241,7 @@ namespace MWWorld // TODO: use a proper btRigidBody / btGhostObject? btVector3 from(pos.x, pos.y, pos.z); btVector3 to(newPos.x, newPos.y, newPos.z); - std::vector > collisions = mPhysEngine.rayTest2(from, to); + std::vector > collisions = mPhysEngine.rayTest2(from, to, OEngine::Physic::CollisionType_Projectile); bool hit=false; for (std::vector >::iterator cIt = collisions.begin(); cIt != collisions.end() && !hit; ++cIt) @@ -227,29 +254,23 @@ namespace MWWorld if (obstacle == caster) continue; - if (obstacle.isEmpty()) + MWWorld::ManualRef projectileRef(MWBase::Environment::get().getWorld()->getStore(), it->mId); + + // Try to get a Ptr to the bow that was used. It might no longer exist. + MWWorld::Ptr bow = projectileRef.getPtr(); + if (!caster.isEmpty()) { - // Terrain - } - else if (obstacle.getClass().isActor()) - { - MWWorld::ManualRef projectileRef(MWBase::Environment::get().getWorld()->getStore(), it->mId); - - // Try to get a Ptr to the bow that was used. It might no longer exist. - MWWorld::Ptr bow = projectileRef.getPtr(); - if (!caster.isEmpty()) - { - MWWorld::InventoryStore& inv = caster.getClass().getInventoryStore(caster); - MWWorld::ContainerStoreIterator invIt = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); - if (invIt != inv.end() && Misc::StringUtils::ciEqual(invIt->getCellRef().getRefId(), it->mBowId)) - bow = *invIt; - } - - if (caster.isEmpty()) - caster = obstacle; - - MWMechanics::projectileHit(caster, obstacle, bow, projectileRef.getPtr(), pos + (newPos - pos) * cIt->first); + MWWorld::InventoryStore& inv = caster.getClass().getInventoryStore(caster); + MWWorld::ContainerStoreIterator invIt = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); + if (invIt != inv.end() && Misc::StringUtils::ciEqual(invIt->getCellRef().getRefId(), it->mBowId)) + bow = *invIt; } + + if (caster.isEmpty()) + caster = obstacle; + + MWMechanics::projectileHit(caster, obstacle, bow, projectileRef.getPtr(), pos + (newPos - pos) * cIt->first); + hit = true; } if (hit) @@ -389,6 +410,7 @@ namespace MWWorld MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); state.mSound = sndMgr->playManualSound3D(esm.mPosition, esm.mSound, 1.0f, 1.0f, MWBase::SoundManager::Play_TypeSfx, MWBase::SoundManager::Play_Loop); + state.mSoundId = esm.mSound; mMagicBolts.push_back(state); return true; diff --git a/apps/openmw/mwworld/projectilemanager.hpp b/apps/openmw/mwworld/projectilemanager.hpp index da965a4cf..6e84b9efb 100644 --- a/apps/openmw/mwworld/projectilemanager.hpp +++ b/apps/openmw/mwworld/projectilemanager.hpp @@ -39,9 +39,10 @@ namespace MWWorld ProjectileManager (Ogre::SceneManager* sceneMgr, OEngine::Physic::PhysicEngine& engine); + /// If caster is an actor, the actor's facing orientation is used. Otherwise fallbackDirection is used. void launchMagicBolt (const std::string& model, const std::string &sound, const std::string &spellId, float speed, bool stack, const ESM::EffectList& effects, - const MWWorld::Ptr& actor, const std::string& sourceName); + const MWWorld::Ptr& caster, const std::string& sourceName, const Ogre::Vector3& fallbackDirection); void launchProjectile (MWWorld::Ptr actor, MWWorld::Ptr projectile, const Ogre::Vector3& pos, const Ogre::Quaternion& orient, MWWorld::Ptr bow, float speed); @@ -64,9 +65,15 @@ namespace MWWorld NifOgre::ObjectScenePtr mObject; Ogre::SceneNode* mNode; - // Actor who shot this projectile int mActorId; + // actorId doesn't work for non-actors, so we also keep track of the Ogre-handle. + // For non-actors, the caster ptr is mainly needed to prevent the projectile + // from colliding with its caster. + // TODO: this will break when the game is saved and reloaded, since there is currently + // no way to write identifiers for non-actors to a savegame. + std::string mCasterHandle; + // MW-id of this projectile std::string mId; }; diff --git a/apps/openmw/mwworld/refdata.cpp b/apps/openmw/mwworld/refdata.cpp index 2e267b37c..f4bc64b70 100644 --- a/apps/openmw/mwworld/refdata.cpp +++ b/apps/openmw/mwworld/refdata.cpp @@ -152,7 +152,8 @@ namespace MWWorld { mLocals.configure (script); mHasLocals = true; - mChanged = true; + if (!mLocals.isEmpty()) + mChanged = true; } } @@ -188,15 +189,25 @@ namespace MWWorld mEnabled = false; } - ESM::Position& RefData::getPosition() + void RefData::setPosition(const ESM::Position& pos) { mChanged = true; + mPosition = pos; + } + + const ESM::Position& RefData::getPosition() + { return mPosition; } - LocalRotation& RefData::getLocalRotation() + void RefData::setLocalRotation(const LocalRotation& rot) { mChanged = true; + mLocalRotation = rot; + } + + const LocalRotation& RefData::getLocalRotation() + { return mLocalRotation; } diff --git a/apps/openmw/mwworld/refdata.hpp b/apps/openmw/mwworld/refdata.hpp index a8ffad684..db66c091b 100644 --- a/apps/openmw/mwworld/refdata.hpp +++ b/apps/openmw/mwworld/refdata.hpp @@ -54,7 +54,7 @@ namespace MWWorld RefData(); /// @param cellRef Used to copy constant data such as position into this class where it can - /// be altered without effecting the original data. This makes it possible + /// be altered without affecting the original data. This makes it possible /// to reset the position as the orignal data is still held in the CellRef RefData (const ESM::CellRef& cellRef); @@ -100,12 +100,14 @@ namespace MWWorld void disable(); - ESM::Position& getPosition(); + void setPosition (const ESM::Position& pos); + const ESM::Position& getPosition(); - LocalRotation& getLocalRotation(); + void setLocalRotation (const LocalRotation& rotation); + const LocalRotation& getLocalRotation(); void setCustomData (CustomData *data); - ///< Set custom data (potentially replacing old custom data). The ownership of \Ʀ data is + ///< Set custom data (potentially replacing old custom data). The ownership of \a data is /// transferred to this. CustomData *getCustomData(); diff --git a/apps/openmw/mwworld/scene.cpp b/apps/openmw/mwworld/scene.cpp index 32bf773bd..6f18a6ef3 100644 --- a/apps/openmw/mwworld/scene.cpp +++ b/apps/openmw/mwworld/scene.cpp @@ -4,10 +4,8 @@ #include -#include - #include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" /// FIXME +#include "../mwbase/world.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/windowmanager.hpp" @@ -22,6 +20,30 @@ namespace { + void updateObjectLocalRotation (const MWWorld::Ptr& ptr, MWWorld::PhysicsSystem& physics, + MWRender::RenderingManager& rendering) + { + if (ptr.getRefData().getBaseNode() != NULL) + { + Ogre::Quaternion worldRotQuat(Ogre::Radian(ptr.getRefData().getPosition().rot[2]), Ogre::Vector3::NEGATIVE_UNIT_Z); + if (!ptr.getClass().isActor()) + worldRotQuat = Ogre::Quaternion(Ogre::Radian(ptr.getRefData().getPosition().rot[0]), Ogre::Vector3::NEGATIVE_UNIT_X)* + Ogre::Quaternion(Ogre::Radian(ptr.getRefData().getPosition().rot[1]), Ogre::Vector3::NEGATIVE_UNIT_Y)* worldRotQuat; + + float x = ptr.getRefData().getLocalRotation().rot[0]; + float y = ptr.getRefData().getLocalRotation().rot[1]; + float z = ptr.getRefData().getLocalRotation().rot[2]; + + Ogre::Quaternion rot(Ogre::Radian(z), Ogre::Vector3::NEGATIVE_UNIT_Z); + if (!ptr.getClass().isActor()) + rot = Ogre::Quaternion(Ogre::Radian(x), Ogre::Vector3::NEGATIVE_UNIT_X)* + Ogre::Quaternion(Ogre::Radian(y), Ogre::Vector3::NEGATIVE_UNIT_Y)*rot; + + ptr.getRefData().getBaseNode()->setOrientation(worldRotQuat*rot); + physics.rotateObject(ptr); + } + } + struct InsertFunctor { MWWorld::CellStore& mCell; @@ -60,13 +82,14 @@ namespace mRendering.addObject (ptr); ptr.getClass().insertObject (ptr, mPhysics); - float ax = Ogre::Radian(ptr.getRefData().getLocalRotation().rot[0]).valueDegrees(); - float ay = Ogre::Radian(ptr.getRefData().getLocalRotation().rot[1]).valueDegrees(); - float az = Ogre::Radian(ptr.getRefData().getLocalRotation().rot[2]).valueDegrees(); - MWBase::Environment::get().getWorld()->localRotateObject (ptr, ax, ay, az); - - MWBase::Environment::get().getWorld()->scaleObject (ptr, ptr.getCellRef().getScale()); - ptr.getClass().adjustPosition (ptr); + updateObjectLocalRotation(ptr, mPhysics, mRendering); + if (ptr.getRefData().getBaseNode()) + { + float scale = ptr.getCellRef().getScale(); + ptr.getClass().adjustScale(ptr, scale); + mRendering.scaleObject(ptr, Ogre::Vector3(scale)); + } + ptr.getClass().adjustPosition (ptr, false); } catch (const std::exception& e) { @@ -85,6 +108,42 @@ namespace namespace MWWorld { + void Scene::updateObjectLocalRotation (const Ptr& ptr) + { + ::updateObjectLocalRotation(ptr, *mPhysics, mRendering); + } + + void Scene::updateObjectRotation (const Ptr& ptr) + { + if(ptr.getRefData().getBaseNode() != 0) + { + mRendering.rotateObject(ptr); + mPhysics->rotateObject(ptr); + } + } + + void Scene::getGridCenter(int &cellX, int &cellY) + { + int maxX = std::numeric_limits::min(); + int maxY = std::numeric_limits::min(); + int minX = std::numeric_limits::max(); + int minY = std::numeric_limits::max(); + CellStoreCollection::iterator iter = mActiveCells.begin(); + while (iter!=mActiveCells.end()) + { + assert ((*iter)->getCell()->isExterior()); + int x = (*iter)->getCell()->getGridX(); + int y = (*iter)->getCell()->getGridY(); + maxX = std::max(x, maxX); + maxY = std::max(y, maxY); + minX = std::min(x, minX); + minY = std::min(y, minY); + ++iter; + } + cellX = (minX + maxX) / 2; + cellY = (minY + maxY) / 2; + } + void Scene::update (float duration, bool paused) { if (mNeedMapUpdate) @@ -94,6 +153,13 @@ namespace MWWorld for (CellStoreCollection::iterator active = mActiveCells.begin(); active!=mActiveCells.end(); ++active) mRendering.requestMap(*active); mNeedMapUpdate = false; + + if (mCurrentCell->isExterior()) + { + int cellX, cellY; + getGridCenter(cellX, cellY); + MWBase::Environment::get().getWindowManager()->setActiveMap(cellX,cellY,false); + } } mRendering.update (duration, paused); @@ -172,6 +238,15 @@ namespace MWWorld insertCell (*cell, true, loadingListener); mRendering.cellAdded (cell); + bool waterEnabled = cell->getCell()->hasWater(); + mRendering.setWaterEnabled(waterEnabled); + if (waterEnabled) + { + mPhysics->enableWater(cell->getWaterLevel()); + mRendering.setWaterHeight(cell->getWaterLevel()); + } + else + mPhysics->disableWater(); mRendering.configureAmbient(*cell); } @@ -181,41 +256,6 @@ namespace MWWorld MWBase::Environment::get().getWorld()->getLocalScripts().addCell (cell); } - void Scene::playerCellChange(CellStore *cell, const ESM::Position& pos, bool adjustPlayerPos) - { - MWBase::World *world = MWBase::Environment::get().getWorld(); - MWWorld::Ptr old = world->getPlayerPtr(); - world->getPlayer().setCell(cell); - - MWWorld::Ptr player = world->getPlayerPtr(); - mRendering.updatePlayerPtr(player); - - if (adjustPlayerPos) { - world->moveObject(player, pos.pos[0], pos.pos[1], pos.pos[2]); - - float x = Ogre::Radian(pos.rot[0]).valueDegrees(); - float y = Ogre::Radian(pos.rot[1]).valueDegrees(); - float z = Ogre::Radian(pos.rot[2]).valueDegrees(); - world->rotateObject(player, x, y, z); - - player.getClass().adjustPosition(player); - } - - MWBase::MechanicsManager *mechMgr = - MWBase::Environment::get().getMechanicsManager(); - - mechMgr->updateCell(old, player); - mechMgr->watchActor(player); - - mRendering.updateTerrain(); - - // Delay the map update until scripts have been given a chance to run. - // If we don't do this, objects that should be disabled will still appear on the map. - mNeedMapUpdate = true; - - MWBase::Environment::get().getWindowManager()->changeCell(mCurrentCell); - } - void Scene::changeToVoid() { CellStoreCollection::iterator active = mActiveCells.begin(); @@ -225,10 +265,29 @@ namespace MWWorld mCurrentCell = NULL; } - void Scene::changeCell (int X, int Y, const ESM::Position& position, bool adjustPlayerPos) + void Scene::playerMoved(const Ogre::Vector3 &pos) { - Nif::NIFFile::CacheLock cachelock; + if (!mCurrentCell || !mCurrentCell->isExterior()) + return; + // figure out the center of the current cell grid (*not* necessarily mCurrentCell, which is the cell the player is in) + int cellX, cellY; + getGridCenter(cellX, cellY); + float centerX, centerY; + MWBase::Environment::get().getWorld()->indexToPosition(cellX, cellY, centerX, centerY, true); + const float maxDistance = 8192/2 + 1024; // 1/2 cell size + threshold + float distance = std::max(std::abs(centerX-pos.x), std::abs(centerY-pos.y)); + if (distance > maxDistance) + { + int newX, newY; + MWBase::Environment::get().getWorld()->positionToIndex(pos.x, pos.y, newX, newY); + changeCellGrid(newX, newY); + mRendering.updateTerrain(); + } + } + + void Scene::changeCellGrid (int X, int Y) + { Loading::Listener* loadingListener = MWBase::Environment::get().getWindowManager()->getLoadingScreen(); Loading::ScopedLoad load(loadingListener); @@ -238,26 +297,6 @@ namespace MWWorld loadingListener->setLabel(loadingExteriorText); CellStoreCollection::iterator active = mActiveCells.begin(); - - // get the number of cells to unload - int numUnload = 0; - while (active!=mActiveCells.end()) - { - if ((*active)->getCell()->isExterior()) - { - if (std::abs (X-(*active)->getCell()->getGridX())<=1 && - std::abs (Y-(*active)->getCell()->getGridY())<=1) - { - // keep cells within the new 3x3 grid - ++active; - continue; - } - } - ++active; - ++numUnload; - } - - active = mActiveCells.begin(); while (active!=mActiveCells.end()) { if ((*active)->getCell()->isExterior()) @@ -276,6 +315,7 @@ namespace MWWorld int refsToLoad = 0; // get the number of refs to load for (int x=X-1; x<=X+1; ++x) + { for (int y=Y-1; y<=Y+1; ++y) { CellStoreCollection::iterator iter = mActiveCells.begin(); @@ -294,11 +334,13 @@ namespace MWWorld if (iter==mActiveCells.end()) refsToLoad += MWBase::Environment::get().getWorld()->getExterior(x, y)->count(); } + } loadingListener->setProgressRange(refsToLoad); // Load cells for (int x=X-1; x<=X+1; ++x) + { for (int y=Y-1; y<=Y+1; ++y) { CellStoreCollection::iterator iter = mActiveCells.begin(); @@ -321,34 +363,47 @@ namespace MWWorld loadCell (cell, loadingListener); } } + } - // find current cell - CellStoreCollection::iterator iter = mActiveCells.begin(); + CellStore* current = MWBase::Environment::get().getWorld()->getExterior(X,Y); + MWBase::Environment::get().getWindowManager()->changeCell(current); - while (iter!=mActiveCells.end()) - { - assert ((*iter)->getCell()->isExterior()); + mCellChanged = true; - if (X==(*iter)->getCell()->getGridX() && - Y==(*iter)->getCell()->getGridY()) - break; + // Delay the map update until scripts have been given a chance to run. + // If we don't do this, objects that should be disabled will still appear on the map. + mNeedMapUpdate = true; + } - ++iter; - } + void Scene::changePlayerCell(CellStore *cell, const ESM::Position &pos, bool adjustPlayerPos) + { + mCurrentCell = cell; - assert (iter!=mActiveCells.end()); + MWBase::World *world = MWBase::Environment::get().getWorld(); + MWWorld::Ptr old = world->getPlayerPtr(); + world->getPlayer().setCell(cell); - mCurrentCell = *iter; + MWWorld::Ptr player = world->getPlayerPtr(); + mRendering.updatePlayerPtr(player); - // adjust player - playerCellChange (mCurrentCell, position, adjustPlayerPos); + if (adjustPlayerPos) { + world->moveObject(player, pos.pos[0], pos.pos[1], pos.pos[2]); - // Sky system - MWBase::Environment::get().getWorld()->adjustSky(); + float x = Ogre::Radian(pos.rot[0]).valueDegrees(); + float y = Ogre::Radian(pos.rot[1]).valueDegrees(); + float z = Ogre::Radian(pos.rot[2]).valueDegrees(); + world->rotateObject(player, x, y, z); - mCellChanged = true; + player.getClass().adjustPosition(player, true); + } - loadingListener->removeWallpaper(); + MWBase::MechanicsManager *mechMgr = + MWBase::Environment::get().getMechanicsManager(); + + mechMgr->updateCell(old, player); + mechMgr->watchActor(player); + + MWBase::Environment::get().getWorld()->adjustSky(); } //We need the ogre renderer and a scene node. @@ -373,21 +428,19 @@ namespace MWWorld void Scene::changeToInteriorCell (const std::string& cellName, const ESM::Position& position) { - Nif::NIFFile::CacheLock lock; - MWBase::Environment::get().getWorld ()->getFader ()->fadeOut(0.5); - - Loading::Listener* loadingListener = MWBase::Environment::get().getWindowManager()->getLoadingScreen(); - Loading::ScopedLoad load(loadingListener); + CellStore *cell = MWBase::Environment::get().getWorld()->getInterior(cellName); + bool loadcell = (mCurrentCell == NULL); + if(!loadcell) + loadcell = *mCurrentCell != *cell; - mRendering.enableTerrain(false); + MWBase::Environment::get().getWindowManager()->fadeScreenOut(0.5); + Loading::Listener* loadingListener = MWBase::Environment::get().getWindowManager()->getLoadingScreen(); std::string loadingInteriorText = "#{sLoadingMessage2}"; loadingListener->setLabel(loadingInteriorText); + Loading::ScopedLoad load(loadingListener); - CellStore *cell = MWBase::Environment::get().getWorld()->getInterior(cellName); - bool loadcell = (mCurrentCell == NULL); - if(!loadcell) - loadcell = *mCurrentCell != *cell; + mRendering.enableTerrain(false); if(!loadcell) { @@ -399,27 +452,16 @@ namespace MWWorld float z = Ogre::Radian(position.rot[2]).valueDegrees(); world->rotateObject(world->getPlayerPtr(), x, y, z); - world->getPlayerPtr().getClass().adjustPosition(world->getPlayerPtr()); - world->getFader()->fadeIn(0.5f); + world->getPlayerPtr().getClass().adjustPosition(world->getPlayerPtr(), true); + MWBase::Environment::get().getWindowManager()->fadeScreenIn(0.5); return; } std::cout << "Changing to interior\n"; - // remove active - CellStoreCollection::iterator active = mActiveCells.begin(); - - // count number of cells to unload - int numUnload = 0; - while (active!=mActiveCells.end()) - { - ++active; - ++numUnload; - } - // unload int current = 0; - active = mActiveCells.begin(); + CellStoreCollection::iterator active = mActiveCells.begin(); while (active!=mActiveCells.end()) { unloadCell (active++); @@ -432,35 +474,39 @@ namespace MWWorld // Load cell. std::cout << "cellName: " << cell->getCell()->mName << std::endl; - //Loading Interior loading text - loadCell (cell, loadingListener); - mCurrentCell = cell; + changePlayerCell(cell, position, true); // adjust fog mRendering.configureFog(*mCurrentCell); - // adjust player - playerCellChange (mCurrentCell, position); - // Sky system MWBase::Environment::get().getWorld()->adjustSky(); mCellChanged = true; - MWBase::Environment::get().getWorld ()->getFader ()->fadeIn(0.5); + MWBase::Environment::get().getWindowManager()->fadeScreenIn(0.5); + + MWBase::Environment::get().getWindowManager()->changeCell(mCurrentCell); - loadingListener->removeWallpaper(); + // Delay the map update until scripts have been given a chance to run. + // If we don't do this, objects that should be disabled will still appear on the map. + mNeedMapUpdate = true; } - void Scene::changeToExteriorCell (const ESM::Position& position) + void Scene::changeToExteriorCell (const ESM::Position& position, bool adjustPlayerPos) { int x = 0; int y = 0; MWBase::Environment::get().getWorld()->positionToIndex (position.pos[0], position.pos[1], x, y); - changeCell (x, y, position, true); + changeCellGrid(x, y); + + CellStore* current = MWBase::Environment::get().getWorld()->getExterior(x, y); + changePlayerCell(current, position, adjustPlayerPos); + + mRendering.updateTerrain(); } CellStore* Scene::getCurrentCell () diff --git a/apps/openmw/mwworld/scene.hpp b/apps/openmw/mwworld/scene.hpp index 449644754..a9d80bf17 100644 --- a/apps/openmw/mwworld/scene.hpp +++ b/apps/openmw/mwworld/scene.hpp @@ -60,11 +60,13 @@ namespace MWWorld bool mNeedMapUpdate; - void playerCellChange (CellStore *cell, const ESM::Position& position, - bool adjustPlayerPos = true); - void insertCell (CellStore &cell, bool rescale, Loading::Listener* loadingListener); + // Load and unload cells as necessary to create a cell grid with "X" and "Y" in the center + void changeCellGrid (int X, int Y); + + void getGridCenter(int& cellX, int& cellY); + public: Scene (MWRender::RenderingManager& rendering, PhysicsSystem *physics); @@ -75,19 +77,21 @@ namespace MWWorld void loadCell (CellStore *cell, Loading::Listener* loadingListener); - void changeCell (int X, int Y, const ESM::Position& position, bool adjustPlayerPos); + void playerMoved (const Ogre::Vector3& pos); - CellStore* getCurrentCell (); + void changePlayerCell (CellStore* newCell, const ESM::Position& position, bool adjustPlayerPos); + + CellStore *getCurrentCell(); const CellStoreCollection& getActiveCells () const; bool hasCellChanged() const; - ///< Has the player moved to a different cell, since the last frame? + ///< Has the set of active cells changed, since the last frame? void changeToInteriorCell (const std::string& cellName, const ESM::Position& position); ///< Move to interior cell. - void changeToExteriorCell (const ESM::Position& position); + void changeToExteriorCell (const ESM::Position& position, bool adjustPlayerPos); ///< Move to exterior cell. void changeToVoid(); @@ -103,6 +107,10 @@ namespace MWWorld void removeObjectFromScene (const Ptr& ptr); ///< Remove an object from the scene, but not from the world model. + void updateObjectLocalRotation (const Ptr& ptr); + + void updateObjectRotation (const Ptr& ptr); + bool isCellActive(const CellStore &cell); Ptr searchPtrViaHandle (const std::string& handle); diff --git a/apps/openmw/mwworld/store.cpp b/apps/openmw/mwworld/store.cpp index 1156cbc15..caf4083fe 100644 --- a/apps/openmw/mwworld/store.cpp +++ b/apps/openmw/mwworld/store.cpp @@ -3,22 +3,8 @@ namespace MWWorld { - -void Store::load(ESM::ESMReader &esm, const std::string &id) +void Store::handleMovedCellRefs(ESM::ESMReader& esm, ESM::Cell* cell) { - // Don't automatically assume that a new cell must be spawned. Multiple plugins write to the same cell, - // and we merge all this data into one Cell object. However, we can't simply search for the cell id, - // as many exterior cells do not have a name. Instead, we need to search by (x,y) coordinates - and they - // are not available until both cells have been loaded! So first, proceed as usual. - - // All cells have a name record, even nameless exterior cells. - std::string idLower = Misc::StringUtils::lowerCase(id); - ESM::Cell *cell = new ESM::Cell; - cell->mName = id; - - //First part of cell loading - cell->preLoad(esm); - //Handling MovedCellRefs, there is no way to do it inside loadcell while (esm.isNextSub("MVRF")) { ESM::CellRef ref; @@ -37,41 +23,69 @@ void Store::load(ESM::ESMReader &esm, const std::string &id) // We should not need to test for duplicates, as this part of the code is pre-cell merge. cell->mMovedRefs.push_back(cMRef); // But there may be duplicates here! - ESM::CellRefTracker::iterator iter = std::find(cellAlt->mLeasedRefs.begin(), cellAlt->mLeasedRefs.end(), ref.mRefNum); - if (iter == cellAlt->mLeasedRefs.end()) - cellAlt->mLeasedRefs.push_back(ref); - else - *iter = ref; + if (!deleted) + { + ESM::CellRefTracker::iterator iter = std::find(cellAlt->mLeasedRefs.begin(), cellAlt->mLeasedRefs.end(), ref.mRefNum); + if (iter == cellAlt->mLeasedRefs.end()) + cellAlt->mLeasedRefs.push_back(ref); + else + *iter = ref; + } } +} + +void Store::load(ESM::ESMReader &esm, const std::string &id) +{ + // Don't automatically assume that a new cell must be spawned. Multiple plugins write to the same cell, + // and we merge all this data into one Cell object. However, we can't simply search for the cell id, + // as many exterior cells do not have a name. Instead, we need to search by (x,y) coordinates - and they + // are not available until both cells have been loaded at least partially! + + // All cells have a name record, even nameless exterior cells. + std::string idLower = Misc::StringUtils::lowerCase(id); + ESM::Cell cell; + cell.mName = id; - //Second part of cell loading - cell->postLoad(esm); + // Load the (x,y) coordinates of the cell, if it is an exterior cell, + // so we can find the cell we need to merge with + cell.loadData(esm); - if(cell->mData.mFlags & ESM::Cell::Interior) + if(cell.mData.mFlags & ESM::Cell::Interior) { // Store interior cell by name, try to merge with existing parent data. ESM::Cell *oldcell = const_cast(search(idLower)); if (oldcell) { - // push the new references on the list of references to manage - oldcell->mContextList.push_back(cell->mContextList.at(0)); - // copy list into new cell - cell->mContextList = oldcell->mContextList; - // have new cell replace old cell - ESM::Cell::merge(oldcell, cell); + // merge new cell into old cell + // push the new references on the list of references to manage (saveContext = true) + oldcell->mData = cell.mData; + oldcell->mName = cell.mName; // merge name just to be sure (ID will be the same, but case could have been changed) + oldcell->loadCell(esm, true); } else - mInt[idLower] = *cell; + { + // spawn a new cell + cell.loadCell(esm, true); + + mInt[idLower] = cell; + } } else { // Store exterior cells by grid position, try to merge with existing parent data. - ESM::Cell *oldcell = const_cast(search(cell->getGridX(), cell->getGridY())); + ESM::Cell *oldcell = const_cast(search(cell.getGridX(), cell.getGridY())); if (oldcell) { + // merge new cell into old cell + oldcell->mData = cell.mData; + oldcell->mName = cell.mName; + oldcell->loadCell(esm, false); + + // handle moved ref (MVRF) subrecords + handleMovedCellRefs (esm, &cell); + // push the new references on the list of references to manage - oldcell->mContextList.push_back(cell->mContextList.at(0)); - // copy list into new cell - cell->mContextList = oldcell->mContextList; + oldcell->postLoad(esm); + // merge lists of leased references, use newer data in case of conflict - for (ESM::MovedCellRefTracker::const_iterator it = cell->mMovedRefs.begin(); it != cell->mMovedRefs.end(); ++it) { + for (ESM::MovedCellRefTracker::const_iterator it = cell.mMovedRefs.begin(); it != cell.mMovedRefs.end(); ++it) { // remove reference from current leased ref tracker and add it to new cell ESM::MovedCellRefTracker::iterator itold = std::find(oldcell->mMovedRefs.begin(), oldcell->mMovedRefs.end(), it->mRefNum); if (itold != oldcell->mMovedRefs.end()) { @@ -81,14 +95,27 @@ void Store::load(ESM::ESMReader &esm, const std::string &id) wipecell->mLeasedRefs.erase(it_lease); *itold = *it; } + else + oldcell->mMovedRefs.push_back(*it); } - cell->mMovedRefs = oldcell->mMovedRefs; - // have new cell replace old cell - ESM::Cell::merge(oldcell, cell); + + // We don't need to merge mLeasedRefs of cell / oldcell. This list is filled when another cell moves a + // reference to this cell, so the list for the new cell should be empty. The list for oldcell, + // however, could have leased refs in it and so should be kept. } else - mExt[std::make_pair(cell->mData.mX, cell->mData.mY)] = *cell; + { + // spawn a new cell + cell.loadCell(esm, false); + + // handle moved ref (MVRF) subrecords + handleMovedCellRefs (esm, &cell); + + // push the new references on the list of references to manage + cell.postLoad(esm); + + mExt[std::make_pair(cell.mData.mX, cell.mData.mY)] = cell; + } } - delete cell; } } diff --git a/apps/openmw/mwworld/store.hpp b/apps/openmw/mwworld/store.hpp index 0f8ab8682..55c5b8191 100644 --- a/apps/openmw/mwworld/store.hpp +++ b/apps/openmw/mwworld/store.hpp @@ -209,8 +209,6 @@ namespace MWWorld } void setUp() { - //std::sort(mStatic.begin(), mStatic.end(), RecordCmp()); - mShared.clear(); mShared.reserve(mStatic.size()); typename std::map::iterator it = mStatic.begin(); @@ -552,23 +550,16 @@ namespace MWWorld template <> class Store : public StoreBase { - struct ExtCmp - { - bool operator()(const ESM::Cell &x, const ESM::Cell &y) { - if (x.mData.mX == y.mData.mX) { - return x.mData.mY < y.mData.mY; - } - return x.mData.mX < y.mData.mX; - } - }; - struct DynamicExtCmp { bool operator()(const std::pair &left, const std::pair &right) const { - if (left.first == right.first) { - return left.second < right.second; - } - return left.first < right.first; + if (left.first == right.first && left.second == right.second) + return false; + + if (left.first == right.first) + return left.second > right.second; + + return left.first > right.first; } }; @@ -591,12 +582,15 @@ namespace MWWorld return search(cell.mName); } + void handleMovedCellRefs(ESM::ESMReader& esm, ESM::Cell* cell); + public: ESMStore *mEsmStore; typedef SharedIterator iterator; Store() + : mEsmStore(NULL) {} const ESM::Cell *search(const std::string &id) const { @@ -680,17 +674,16 @@ namespace MWWorld } void setUp() { - //typedef std::vector::iterator Iterator; typedef DynamicExt::iterator ExtIterator; typedef std::map::iterator IntIterator; - //std::sort(mInt.begin(), mInt.end(), RecordCmp()); + mSharedInt.clear(); mSharedInt.reserve(mInt.size()); for (IntIterator it = mInt.begin(); it != mInt.end(); ++it) { mSharedInt.push_back(&(it->second)); } - //std::sort(mExt.begin(), mExt.end(), ExtCmp()); + mSharedExt.clear(); mSharedExt.reserve(mExt.size()); for (ExtIterator it = mExt.begin(); it != mExt.end(); ++it) { mSharedExt.push_back(&(it->second)); @@ -1150,6 +1143,56 @@ namespace MWWorld } }; + + // Specialisation for ESM::Spell to preserve record order as it was in the content files. + // The NPC spell autocalc code heavily depends on this order. + // We could also do this in the base class, but it's usually not a good idea to depend on record order. + template<> + inline void Store::clearDynamic() + { + // remove the dynamic part of mShared + mShared.erase(mShared.begin() + mStatic.size(), mShared.end()); + mDynamic.clear(); + } + + template<> + inline void Store::load(ESM::ESMReader &esm, const std::string &id) { + std::string idLower = Misc::StringUtils::lowerCase(id); + + std::pair inserted = mStatic.insert(std::make_pair(idLower, ESM::Spell())); + if (inserted.second) + mShared.push_back(&mStatic[idLower]); + + inserted.first->second.mId = idLower; + inserted.first->second.load(esm); + } + + template<> + inline void Store::setUp() + { + // remove the dynamic part of mShared + mShared.erase(mShared.begin() + mStatic.size(), mShared.end()); + } + + template<> + inline void Store::setUp() + { + // DialInfos marked as deleted are kept during the loading phase, so that the linked list + // structure is kept intact for inserting further INFOs. Delete them now that loading is done. + for (Static::iterator it = mStatic.begin(); it != mStatic.end(); ++it) + { + ESM::Dialogue& dial = it->second; + dial.clearDeletedInfos(); + } + + mShared.clear(); + mShared.reserve(mStatic.size()); + std::map::iterator it = mStatic.begin(); + for (; it != mStatic.end(); ++it) { + mShared.push_back(&(it->second)); + } + } + } //end namespace #endif diff --git a/apps/openmw/mwworld/weather.cpp b/apps/openmw/mwworld/weather.cpp index 25f523bee..f738734b1 100644 --- a/apps/openmw/mwworld/weather.cpp +++ b/apps/openmw/mwworld/weather.cpp @@ -58,6 +58,23 @@ void WeatherManager::setFallbackWeather(Weather& weather,const std::string& name weather.mWindSpeed = mFallback->getFallbackFloat("Weather_"+upper+"_Wind_Speed"); weather.mCloudSpeed = mFallback->getFallbackFloat("Weather_"+upper+"_Cloud_Speed"); weather.mGlareView = mFallback->getFallbackFloat("Weather_"+upper+"_Glare_View"); + weather.mCloudTexture = mFallback->getFallbackString("Weather_"+upper+"_Cloud_Texture"); + + bool usesPrecip = mFallback->getFallbackBool("Weather_"+upper+"_Using_Precip"); + if (usesPrecip) + weather.mRainEffect = "meshes\\raindrop.nif"; + weather.mRainSpeed = mRainSpeed; + weather.mRainFrequency = mFallback->getFallbackFloat("Weather_"+upper+"_Rain_Entrance_Speed"); + /* +Unhandled: +Rain Diameter=600 ? +Rain Height Min=200 ? +Rain Height Max=700 ? +Rain Threshold=0.6 ? +Max Raindrops=650 ? +*/ + weather.mIsStorm = (name == "ashstorm" || name == "blight"); + mWeatherSettings[name] = weather; } @@ -93,7 +110,8 @@ WeatherManager::WeatherManager(MWRender::RenderingManager* rendering,MWWorld::Fa mHour(14), mCurrentWeather("clear"), mNextWeather(""), mFirstUpdate(true), mWeatherUpdateTime(0), mThunderFlash(0), mThunderChance(0), mThunderChanceNeeded(50), mThunderSoundDelay(0), mRemainingTransitionTime(0), - mTimePassed(0), mFallback(fallback), mWindSpeed(0.f), mRendering(rendering) + mTimePassed(0), mFallback(fallback), mWindSpeed(0.f), mRendering(rendering), mIsStorm(false), + mStormDirection(0,1,0) { //Globals mThunderSoundID0 = mFallback->getFallbackString("Weather_Thunderstorm_Thunder_Sound_ID_0"); @@ -110,6 +128,8 @@ WeatherManager::WeatherManager(MWRender::RenderingManager* rendering,MWWorld::Fa mThunderThreshold = mFallback->getFallbackFloat("Weather_Thunderstorm_Thunder_Threshold"); mThunderSoundDelay = 0.25; + mRainSpeed = mFallback->getFallbackFloat("Weather_Precip_Gravity"); + //Some useful values /* TODO: Use pre-sunrise_time, pre-sunset_time, * post-sunrise_time, and post-sunset_time to better @@ -123,48 +143,44 @@ WeatherManager::WeatherManager(MWRender::RenderingManager* rendering,MWWorld::Fa //Weather Weather clear; - clear.mCloudTexture = "tx_sky_clear.dds"; setFallbackWeather(clear,"clear"); Weather cloudy; - cloudy.mCloudTexture = "tx_sky_cloudy.dds"; setFallbackWeather(cloudy,"cloudy"); Weather foggy; - foggy.mCloudTexture = "tx_sky_foggy.dds"; setFallbackWeather(foggy,"foggy"); Weather thunderstorm; - thunderstorm.mCloudTexture = "tx_sky_thunder.dds"; thunderstorm.mRainLoopSoundID = "rain heavy"; + thunderstorm.mRainEffect = "meshes\\raindrop.nif"; setFallbackWeather(thunderstorm,"thunderstorm"); Weather rain; - rain.mCloudTexture = "tx_sky_rainy.dds"; rain.mRainLoopSoundID = "rain"; + rain.mRainEffect = "meshes\\raindrop.nif"; setFallbackWeather(rain,"rain"); Weather overcast; - overcast.mCloudTexture = "tx_sky_overcast.dds"; setFallbackWeather(overcast,"overcast"); Weather ashstorm; - ashstorm.mCloudTexture = "tx_sky_ashstorm.dds"; ashstorm.mAmbientLoopSoundID = "ashstorm"; + ashstorm.mParticleEffect = "meshes\\ashcloud.nif"; setFallbackWeather(ashstorm,"ashstorm"); Weather blight; - blight.mCloudTexture = "tx_sky_blight.dds"; blight.mAmbientLoopSoundID = "blight"; + blight.mParticleEffect = "meshes\\blightcloud.nif"; setFallbackWeather(blight,"blight"); Weather snow; - snow.mCloudTexture = "tx_bm_sky_snow.dds"; + snow.mParticleEffect = "meshes\\snow.nif"; setFallbackWeather(snow, "snow"); Weather blizzard; - blizzard.mCloudTexture = "tx_bm_sky_blizzard.dds"; blizzard.mAmbientLoopSoundID = "BM Blizzard"; + blizzard.mParticleEffect = "meshes\\blizzard.nif"; setFallbackWeather(blizzard,"blizzard"); } @@ -214,6 +230,14 @@ void WeatherManager::setResult(const String& weatherType) mResult.mAmbientLoopSoundID = current.mAmbientLoopSoundID; mResult.mSunColor = current.mSunDiscSunsetColor; + mResult.mIsStorm = current.mIsStorm; + + mResult.mRainSpeed = current.mRainSpeed; + mResult.mRainFrequency = current.mRainFrequency; + + mResult.mParticleEffect = current.mParticleEffect; + mResult.mRainEffect = current.mRainEffect; + mResult.mNight = (mHour < mSunriseTime || mHour > mNightStart - 1); mResult.mFogDepth = mResult.mNight ? current.mLandFogNightDepth : current.mLandFogDayDepth; @@ -316,9 +340,15 @@ void WeatherManager::transition(float factor) mResult.mNightFade = lerp(current.mNightFade, other.mNightFade, factor); mResult.mNight = current.mNight; + + mResult.mIsStorm = current.mIsStorm; + mResult.mParticleEffect = current.mParticleEffect; + mResult.mRainEffect = current.mRainEffect; + mResult.mRainSpeed = current.mRainSpeed; + mResult.mRainFrequency = current.mRainFrequency; } -void WeatherManager::update(float duration) +void WeatherManager::update(float duration, bool paused) { float timePassed = mTimePassed; mTimePassed = 0; @@ -353,6 +383,18 @@ void WeatherManager::update(float duration) setResult(mCurrentWeather); mWindSpeed = mResult.mWindSpeed; + mIsStorm = mResult.mIsStorm; + + if (mIsStorm) + { + MWWorld::Ptr player = world->getPlayerPtr(); + Ogre::Vector3 playerPos (player.getRefData().getPosition().pos); + Ogre::Vector3 redMountainPos (19950, 72032, 27831); + + mStormDirection = (playerPos - redMountainPos); + mStormDirection.z = 0; + mRendering->getSkyManager()->setStormDirection(mStormDirection); + } mRendering->configureFog(mResult.mFogDepth, mResult.mFogColor); @@ -378,11 +420,13 @@ void WeatherManager::update(float duration) int facing = (mHour > 13.f) ? 1 : -1; + bool sun_is_moon = mHour >= mNightStart || mHour <= mSunriseTime; + Vector3 final( (height - 1) * facing, (height - 1) * facing, height); - mRendering->setSunDirection(final); + mRendering->setSunDirection(final, sun_is_moon); /* * TODO: import separated fadeInStart/Finish, fadeOutStart/Finish @@ -439,51 +483,56 @@ void WeatherManager::update(float duration) mRendering->getSkyManager()->secundaDisable(); } - if (mCurrentWeather == "thunderstorm" && mNextWeather == "") + if (!paused) { - if (mThunderFlash > 0) + if (mCurrentWeather == "thunderstorm" && mNextWeather == "") { - // play the sound after a delay - mThunderSoundDelay -= duration; - if (mThunderSoundDelay <= 0) - { - // pick a random sound - int sound = rand() % 4; - std::string* soundName = NULL; - if (sound == 0) soundName = &mThunderSoundID0; - else if (sound == 1) soundName = &mThunderSoundID1; - else if (sound == 2) soundName = &mThunderSoundID2; - else if (sound == 3) soundName = &mThunderSoundID3; - MWBase::Environment::get().getSoundManager()->playSound(*soundName, 1.0, 1.0); - mThunderSoundDelay = 1000; - } - - mThunderFlash -= duration; if (mThunderFlash > 0) - mRendering->getSkyManager()->setLightningStrength( mThunderFlash / mThunderThreshold ); - else { - mThunderChanceNeeded = rand() % 100; - mThunderChance = 0; - mRendering->getSkyManager()->setLightningStrength( 0.f ); + // play the sound after a delay + mThunderSoundDelay -= duration; + if (mThunderSoundDelay <= 0) + { + // pick a random sound + int sound = rand() % 4; + std::string* soundName = NULL; + if (sound == 0) soundName = &mThunderSoundID0; + else if (sound == 1) soundName = &mThunderSoundID1; + else if (sound == 2) soundName = &mThunderSoundID2; + else if (sound == 3) soundName = &mThunderSoundID3; + if (soundName) + MWBase::Environment::get().getSoundManager()->playSound(*soundName, 1.0, 1.0); + mThunderSoundDelay = 1000; + } + + mThunderFlash -= duration; + if (mThunderFlash > 0) + mRendering->getSkyManager()->setLightningStrength( mThunderFlash / mThunderThreshold ); + else + { + mThunderChanceNeeded = rand() % 100; + mThunderChance = 0; + mRendering->getSkyManager()->setLightningStrength( 0.f ); + } } - } - else - { - // no thunder active - mThunderChance += duration*4; // chance increases by 4 percent every second - if (mThunderChance >= mThunderChanceNeeded) + else { - mThunderFlash = mThunderThreshold; + // no thunder active + mThunderChance += duration*4; // chance increases by 4 percent every second + if (mThunderChance >= mThunderChanceNeeded) + { + mThunderFlash = mThunderThreshold; - mRendering->getSkyManager()->setLightningStrength( mThunderFlash / mThunderThreshold ); + mRendering->getSkyManager()->setLightningStrength( mThunderFlash / mThunderThreshold ); - mThunderSoundDelay = 0.25; + mThunderSoundDelay = 0.25; + } } } + else + mRendering->getSkyManager()->setLightningStrength(0.f); } - else - mRendering->getSkyManager()->setLightningStrength(0.f); + mRendering->setAmbientColour(mResult.mAmbientColor); mRendering->sunEnable(false); @@ -491,7 +540,6 @@ void WeatherManager::update(float duration) mRendering->getSkyManager()->setWeather(mResult); - // Play sounds if (mNextWeather == "") { @@ -659,9 +707,13 @@ void WeatherManager::changeWeather(const std::string& region, const unsigned int mRegionOverrides[Misc::StringUtils::lowerCase(region)] = weather; - std::string playerRegion = MWBase::Environment::get().getWorld()->getPlayerPtr().getCell()->getCell()->mRegion; - if (Misc::StringUtils::ciEqual(region, playerRegion)) - setWeather(weather); + MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); + if (player.isInCell()) + { + std::string playerRegion = player.getCell()->getCell()->mRegion; + if (Misc::StringUtils::ciEqual(region, playerRegion)) + setWeather(weather); + } } void WeatherManager::modRegion(const std::string ®ionid, const std::vector &chances) @@ -768,3 +820,13 @@ void WeatherManager::switchToNextWeather(bool instantly) } } } + +bool WeatherManager::isInStorm() const +{ + return mIsStorm; +} + +Ogre::Vector3 WeatherManager::getStormDirection() const +{ + return mStormDirection; +} diff --git a/apps/openmw/mwworld/weather.hpp b/apps/openmw/mwworld/weather.hpp index 3e9df504b..97897fda9 100644 --- a/apps/openmw/mwworld/weather.hpp +++ b/apps/openmw/mwworld/weather.hpp @@ -5,6 +5,7 @@ #include #include +#include namespace ESM { @@ -57,7 +58,15 @@ namespace MWWorld bool mNight; // use night skybox float mNightFade; // fading factor for night skybox + bool mIsStorm; + std::string mAmbientLoopSoundID; + + std::string mParticleEffect; + + std::string mRainEffect; + float mRainSpeed; + float mRainFrequency; }; @@ -100,7 +109,7 @@ namespace MWWorld // Duration of weather transition (in days) float mTransitionDelta; - // No idea what this one is used for? + // Used by scripts to animate signs, etc based on the wind (GetWindSpeed) float mWindSpeed; // Cloud animation speed multiplier @@ -119,7 +128,25 @@ namespace MWWorld // Rain sound effect std::string mRainLoopSoundID; - /// \todo disease chance + // Is this an ash storm / blight storm? If so, the following will happen: + // - The particles and clouds will be oriented so they appear to come from the Red Mountain. + // - Characters will animate their hand to protect eyes from the storm when looking in its direction (idlestorm animation) + // - Slower movement when walking against the storm (fStromWalkMult) + bool mIsStorm; + + // How fast does rain travel down? + // In Morrowind.ini this is set globally, but we may want to change it per weather later. + float mRainSpeed; + + // How often does a new rain mesh spawn? + float mRainFrequency; + + std::string mParticleEffect; + + std::string mRainEffect; + + // Note: For Weather Blight, there is a "Disease Chance" (=0.1) setting. But according to MWSFD this feature + // is broken in the vanilla game and was disabled. }; /// @@ -142,8 +169,9 @@ namespace MWWorld /** * Per-frame update * @param duration + * @param paused */ - void update(float duration); + void update(float duration, bool paused = false); void stopSounds(bool stopAll); @@ -151,6 +179,11 @@ namespace MWWorld float getWindSpeed() const; + /// Are we in an ash or blight storm? + bool isInStorm() const; + + Ogre::Vector3 getStormDirection() const; + void advanceTime(double hours) { mTimePassed += hours*3600; @@ -170,6 +203,9 @@ namespace MWWorld private: float mHour; float mWindSpeed; + bool mIsStorm; + Ogre::Vector3 mStormDirection; + MWWorld::Fallback* mFallback; void setFallbackWeather(Weather& weather,const std::string& name); MWRender::RenderingManager* mRendering; @@ -208,6 +244,7 @@ namespace MWWorld typedef std::map > RegionModMap; RegionModMap mRegionMods; + float mRainSpeed; float mSunriseTime; float mSunsetTime; float mSunriseDuration; diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index c95a137ec..4c2bd669b 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -1,4 +1,5 @@ #include "worldimp.hpp" + #ifdef _WIN32 #include #elif defined HAVE_UNORDERED_MAP @@ -36,6 +37,8 @@ #include "../mwrender/sky.hpp" #include "../mwrender/animation.hpp" +#include "../mwscript/interpretercontext.hpp" + #include "../mwclass/door.hpp" #include "player.hpp" @@ -48,10 +51,23 @@ #include "contentloader.hpp" #include "esmloader.hpp" -#include "omwloader.hpp" using namespace Ogre; +namespace +{ + +// Wraps a value to (-PI, PI] +void wrap(float& rad) +{ + if (rad>0) + rad = std::fmod(rad+Ogre::Math::PI, 2.0f*Ogre::Math::PI)-Ogre::Math::PI; + else + rad = std::fmod(rad-Ogre::Math::PI, 2.0f*Ogre::Math::PI)+Ogre::Math::PI; +} + +} + namespace MWWorld { struct GameContentLoader : public ContentLoader @@ -131,8 +147,8 @@ namespace MWWorld mSky (true), mCells (mStore, mEsm), mActivationDistanceOverride (activationDistanceOverride), mFallback(fallbackMap), mTeleportEnabled(true), mLevitationEnabled(true), - mFacedDistance(FLT_MAX), mGodMode(false), mContentFiles (contentFiles), - mGoToJail(false), + mGodMode(false), mContentFiles (contentFiles), + mGoToJail(false), mDaysInPrison(0), mStartCell (startCell), mStartupScript(startupScript) { mPhysics = new PhysicsSystem(renderer); @@ -146,19 +162,18 @@ namespace MWWorld mWeatherManager = new MWWorld::WeatherManager(mRendering,&mFallback); - // NOTE: We might need to reserve one more for the running game / save. mEsm.resize(contentFiles.size()); Loading::Listener* listener = MWBase::Environment::get().getWindowManager()->getLoadingScreen(); listener->loadingOn(); GameContentLoader gameContentLoader(*listener); EsmLoader esmLoader(mStore, mEsm, encoder, *listener); - OmwLoader omwLoader(*listener); gameContentLoader.addLoader(".esm", &esmLoader); gameContentLoader.addLoader(".esp", &esmLoader); - gameContentLoader.addLoader(".omwgame", &omwLoader); - gameContentLoader.addLoader(".omwaddon", &omwLoader); + gameContentLoader.addLoader(".omwgame", &esmLoader); + gameContentLoader.addLoader(".omwaddon", &esmLoader); + gameContentLoader.addLoader(".project", &esmLoader); loadContentFiles(fileCollections, contentFiles, gameContentLoader); @@ -194,9 +209,9 @@ namespace MWWorld // set new game mark mGlobalVariables["chargenstate"].setInteger (1); mGlobalVariables["pcrace"].setInteger (3); - - MWBase::Environment::get().getScriptManager()->getGlobalScripts().addStartup(); } + else + mGlobalVariables["chargenstate"].setInteger (-1); if (bypass && !mStartCell.empty()) { @@ -226,7 +241,7 @@ namespace MWWorld pos.rot[0] = 0; pos.rot[1] = 0; pos.rot[2] = 0; - mWorldScene->changeToExteriorCell(pos); + mWorldScene->changeToExteriorCell(pos, true); } } @@ -242,7 +257,8 @@ namespace MWWorld mWeatherManager = 0; mWeatherManager = new MWWorld::WeatherManager(mRendering,&mFallback); - MWBase::Environment::get().getWindowManager()->executeInConsole(mStartupScript); + if (!mStartupScript.empty()) + MWBase::Environment::get().getWindowManager()->executeInConsole(mStartupScript); } void World::clear() @@ -252,7 +268,6 @@ namespace MWWorld mProjectileManager->clear(); mLocalScripts.clear(); - mPlayer->clear(); mWorldScene->changeToVoid(); @@ -261,9 +276,10 @@ namespace MWWorld if (mPlayer) { - mPlayer->setCell (0); + mPlayer->clear(); + mPlayer->setCell(0); mPlayer->getPlayer().getRefData() = RefData(); - mPlayer->set (mStore.get().find ("player")); + mPlayer->set(mStore.get().find ("player")); } mCells.clear(); @@ -273,7 +289,7 @@ namespace MWWorld mGodMode = false; mSky = true; mTeleportEnabled = true; - mFacedDistance = FLT_MAX; + mLevitationEnabled = true; mGlobalVariables.fill (mStore); } @@ -287,7 +303,8 @@ namespace MWWorld +mProjectileManager->countSavedGameRecords() +1 // player record +1 // weather record - +1; // actorId counter + +1 // actorId counter + +1; // levitation/teleport enabled state } void World::write (ESM::ESMWriter& writer, Loading::Listener& progress) const @@ -303,71 +320,119 @@ namespace MWWorld MWMechanics::CreatureStats::writeActorIdCounter(writer); progress.increaseProgress(); + mStore.write (writer, progress); // dynamic Store must be written (and read) before Cells, so that + // references to custom made records will be recognized mCells.write (writer, progress); - mStore.write (writer, progress); mGlobalVariables.write (writer, progress); mPlayer->write (writer, progress); mWeatherManager->write (writer, progress); mProjectileManager->write (writer, progress); + + writer.startRecord(ESM::REC_ENAB); + writer.writeHNT("TELE", mTeleportEnabled); + writer.writeHNT("LEVT", mLevitationEnabled); + writer.endRecord(ESM::REC_ENAB); + progress.increaseProgress(); } void World::readRecord (ESM::ESMReader& reader, int32_t type, const std::map& contentFileMap) { - if (type == ESM::REC_ACTC) - { - MWMechanics::CreatureStats::readActorIdCounter(reader); - return; - } - - if (!mStore.readRecord (reader, type) && - !mGlobalVariables.readRecord (reader, type) && - !mPlayer->readRecord (reader, type) && - !mWeatherManager->readRecord (reader, type) && - !mCells.readRecord (reader, type, contentFileMap) && - !mProjectileManager->readRecord (reader, type)) + switch (type) { - throw std::runtime_error ("unknown record in saved game"); + case ESM::REC_ACTC: + MWMechanics::CreatureStats::readActorIdCounter(reader); + return; + case ESM::REC_ENAB: + reader.getHNT(mTeleportEnabled, "TELE"); + reader.getHNT(mLevitationEnabled, "LEVT"); + return; + default: + if (!mStore.readRecord (reader, type) && + !mGlobalVariables.readRecord (reader, type) && + !mPlayer->readRecord (reader, type) && + !mWeatherManager->readRecord (reader, type) && + !mCells.readRecord (reader, type, contentFileMap) && + !mProjectileManager->readRecord (reader, type)) + { + throw std::runtime_error ("unknown record in saved game"); + } + break; } } void World::ensureNeededRecords() { - if (!mStore.get().search("sCompanionShare")) - { - ESM::GameSetting sCompanionShare; - sCompanionShare.mId = "sCompanionShare"; - ESM::Variant value; - value.setType(ESM::VT_String); - value.setString("Companion Share"); - sCompanionShare.mValue = value; - mStore.insertStatic(sCompanionShare); - } - if (!mStore.get().search("dayspassed")) - { - // vanilla Morrowind does not define dayspassed. - ESM::Global dayspassed; - dayspassed.mId = "dayspassed"; - ESM::Variant value; - value.setType(ESM::VT_Long); - value.setInteger(1); // but the addons start counting at 1 :( - dayspassed.mValue = value; - mStore.insertStatic(dayspassed); + std::map gmst; + // Companion (tribunal) + gmst["sCompanionShare"] = ESM::Variant("Companion Share"); + gmst["sCompanionWarningMessage"] = ESM::Variant("Warning message"); + gmst["sCompanionWarningButtonOne"] = ESM::Variant("Button 1"); + gmst["sCompanionWarningButtonTwo"] = ESM::Variant("Button 2"); + gmst["sCompanionShare"] = ESM::Variant("Companion Share"); + gmst["sProfitValue"] = ESM::Variant("Profit Value"); + gmst["sTeleportDisabled"] = ESM::Variant("Teleport disabled"); + gmst["sLevitateDisabled"] = ESM::Variant("Levitate disabled"); + + // Missing in unpatched MW 1.0 + gmst["sDifficulty"] = ESM::Variant("Difficulty"); + gmst["fDifficultyMult"] = ESM::Variant(5.f); + gmst["sAuto_Run"] = ESM::Variant("Auto Run"); + gmst["sServiceRefusal"] = ESM::Variant("Service Refusal"); + gmst["sNeedOneSkill"] = ESM::Variant("Need one skill"); + gmst["sNeedTwoSkills"] = ESM::Variant("Need two skills"); + gmst["sEasy"] = ESM::Variant("Easy"); + gmst["sHard"] = ESM::Variant("Hard"); + gmst["sDeleteNote"] = ESM::Variant("Delete Note"); + gmst["sEditNote"] = ESM::Variant("Edit Note"); + gmst["sAdmireSuccess"] = ESM::Variant("Admire Success"); + gmst["sAdmireFail"] = ESM::Variant("Admire Fail"); + gmst["sIntimidateSuccess"] = ESM::Variant("Intimidate Success"); + gmst["sIntimidateFail"] = ESM::Variant("Intimidate Fail"); + gmst["sTauntSuccess"] = ESM::Variant("Taunt Success"); + gmst["sTauntFail"] = ESM::Variant("Taunt Fail"); + gmst["sBribeSuccess"] = ESM::Variant("Bribe Success"); + gmst["sBribeFail"] = ESM::Variant("Bribe Fail"); + + // Werewolf (BM) + gmst["fWereWolfRunMult"] = ESM::Variant(1.f); + gmst["fWereWolfSilverWeaponDamageMult"] = ESM::Variant(1.f); + + + std::map globals; + // vanilla Morrowind does not define dayspassed. + globals["dayspassed"] = ESM::Variant(1); // but the addons start counting at 1 :( + globals["werewolfclawmult"] = ESM::Variant(25.f); + globals["pcknownwerewolf"] = ESM::Variant(0); + + for (std::map::iterator it = gmst.begin(); it != gmst.end(); ++it) + { + if (!mStore.get().search(it->first)) + { + ESM::GameSetting setting; + setting.mId = it->first; + setting.mValue = it->second; + mStore.insertStatic(setting); + } } - if (!mStore.get().search("fWereWolfRunMult")) + + for (std::map::iterator it = globals.begin(); it != globals.end(); ++it) { - ESM::GameSetting fWereWolfRunMult; - fWereWolfRunMult.mId = "fWereWolfRunMult"; - ESM::Variant value; - value.setType(ESM::VT_Float); - value.setFloat(1.f); - fWereWolfRunMult.mValue = value; - mStore.insertStatic(fWereWolfRunMult); + if (!mStore.get().search(it->first)) + { + ESM::Global setting; + setting.mId = it->first; + setting.mValue = it->second; + mStore.insertStatic(setting); + } } } World::~World() { + // Must be cleared before mRendering is destroyed + mProjectileManager->clear(); + delete mWeatherManager; delete mWorldScene; delete mRendering; @@ -429,7 +494,7 @@ namespace MWWorld mRendering->getCamera()->toggleVanityMode(false); } if(mRendering->getCamera()->isFirstPerson()) - togglePOV(); + mRendering->getCamera()->toggleViewMode(true); } MWWorld::Player& World::getPlayer() @@ -526,28 +591,37 @@ namespace MWWorld std::string lowerCaseName = Misc::StringUtils::lowerCase(name); - // active cells for (Scene::CellStoreCollection::const_iterator iter (mWorldScene->getActiveCells().begin()); iter!=mWorldScene->getActiveCells().end(); ++iter) { + // TODO: caching still doesn't work efficiently here (only works for the one CellStore that the reference is in) CellStore* cellstore = *iter; - Ptr ptr = mCells.getPtr (lowerCaseName, *cellstore, true); + Ptr ptr = mCells.getPtr (lowerCaseName, *cellstore, false); if (!ptr.isEmpty()) return ptr; } - Ptr ptr = mPlayer->getPlayer().getClass() - .getContainerStore(mPlayer->getPlayer()).search(lowerCaseName); - - if (!ptr.isEmpty()) - return ptr; - if (!activeOnly) { ret = mCells.getPtr (lowerCaseName); + if (!ret.isEmpty()) + return ret; } - return ret; + + for (Scene::CellStoreCollection::const_iterator iter (mWorldScene->getActiveCells().begin()); + iter!=mWorldScene->getActiveCells().end(); ++iter) + { + CellStore* cellstore = *iter; + Ptr ptr = cellstore->searchInContainer(lowerCaseName); + if (!ptr.isEmpty()) + return ptr; + } + + Ptr ptr = mPlayer->getPlayer().getClass() + .getContainerStore(mPlayer->getPlayer()).search(lowerCaseName); + + return ptr; } Ptr World::getPtr (const std::string& name, bool activeOnly) @@ -583,6 +657,47 @@ namespace MWWorld return mWorldScene->searchPtrViaActorId (actorId); } + struct FindContainerFunctor + { + Ptr mContainedPtr; + Ptr mResult; + + FindContainerFunctor(const Ptr& containedPtr) : mContainedPtr(containedPtr) {} + + bool operator() (Ptr ptr) + { + if (mContainedPtr.getContainerStore() == &ptr.getClass().getContainerStore(ptr)) + { + mResult = ptr; + return false; + } + + return true; + } + }; + + Ptr World::findContainer(const Ptr& ptr) + { + if (ptr.isInCell()) + return Ptr(); + + Ptr player = getPlayerPtr(); + if (ptr.getContainerStore() == &player.getClass().getContainerStore(player)) + return player; + + const Scene::CellStoreCollection& collection = mWorldScene->getActiveCells(); + for (Scene::CellStoreCollection::const_iterator cellIt = collection.begin(); cellIt != collection.end(); ++cellIt) + { + FindContainerFunctor functor(ptr); + (*cellIt)->forEachContainer(functor); + + if (!functor.mResult.isEmpty()) + return functor.mResult; + } + + return Ptr(); + } + void World::addContainerScripts(const Ptr& reference, CellStore * cell) { if( reference.getTypeName()==typeid (ESM::Container).name() || @@ -820,6 +935,8 @@ namespace MWWorld void World::changeToInteriorCell (const std::string& cellName, const ESM::Position& position) { + mPhysics->clearQueuedMovement(); + if (mCurrentWorldSpace != cellName) { // changed worldspace @@ -835,6 +952,8 @@ namespace MWWorld void World::changeToExteriorCell (const ESM::Position& position) { + mPhysics->clearQueuedMovement(); + if (mCurrentWorldSpace != "sys::default") // FIXME { // changed worldspace @@ -842,7 +961,7 @@ namespace MWWorld mRendering->notifyWorldSpaceChanged(); } removeContainerScripts(getPlayerPtr()); - mWorldScene->changeToExteriorCell(position); + mWorldScene->changeToExteriorCell(position, true); addContainerScripts(getPlayerPtr(), getPlayerPtr().getCell()); } @@ -888,9 +1007,27 @@ namespace MWWorld MWWorld::Ptr World::getFacedObject() { - if (mFacedHandle.empty()) + std::string facedHandle; + + if (MWBase::Environment::get().getWindowManager()->isGuiMode() && + MWBase::Environment::get().getWindowManager()->isConsoleMode()) + getFacedHandle(facedHandle, getMaxActivationDistance() * 50, false); + else + { + float telekinesisRangeBonus = + mPlayer->getPlayer().getClass().getCreatureStats(mPlayer->getPlayer()).getMagicEffects() + .get(ESM::MagicEffect::Telekinesis).getMagnitude(); + telekinesisRangeBonus = feetToGameUnits(telekinesisRangeBonus); + + float activationDistance = getMaxActivationDistance() + telekinesisRangeBonus; + + getFacedHandle(facedHandle, activationDistance); + } + + if (facedHandle.empty()) return MWWorld::Ptr(); - return searchPtrViaHandle(mFacedHandle); + + return getPtrViaHandle(facedHandle); } std::pair World::getHitContact(const MWWorld::Ptr &ptr, float distance) @@ -937,17 +1074,19 @@ namespace MWWorld void World::moveObject(const Ptr &ptr, CellStore* newCell, float x, float y, float z) { - ESM::Position &pos = ptr.getRefData().getPosition(); + ESM::Position pos = ptr.getRefData().getPosition(); pos.pos[0] = x; pos.pos[1] = y; pos.pos[2] = z; + ptr.getRefData().setPosition(pos); + Ogre::Vector3 vec(x, y, z); - CellStore *currCell = ptr.isInCell() ? ptr.getCell() : NULL; + CellStore *currCell = ptr.isInCell() ? ptr.getCell() : NULL; // currCell == NULL should only happen for player, during initial startup bool isPlayer = ptr == mPlayer->getPlayer(); - bool haveToMove = isPlayer || mWorldScene->isCellActive(*currCell); + bool haveToMove = isPlayer || (currCell && mWorldScene->isCellActive(*currCell)); if (currCell != newCell) { @@ -959,17 +1098,29 @@ namespace MWWorld changeToInteriorCell(Misc::StringUtils::lowerCase(newCell->getCell()->mName), pos); else { - int cellX = newCell->getCell()->getGridX(); - int cellY = newCell->getCell()->getGridY(); - mWorldScene->changeCell(cellX, cellY, pos, false); + if (mWorldScene->isCellActive(*newCell)) + mWorldScene->changePlayerCell(newCell, pos, false); + else + mWorldScene->changeToExteriorCell(pos, false); } addContainerScripts (getPlayerPtr(), newCell); } else { - if (!mWorldScene->isCellActive(*currCell)) - ptr.getClass().copyToCell(ptr, *newCell, pos); - else if (!mWorldScene->isCellActive(*newCell)) + bool currCellActive = mWorldScene->isCellActive(*currCell); + bool newCellActive = mWorldScene->isCellActive(*newCell); + if (!currCellActive && newCellActive) + { + MWWorld::Ptr newPtr = ptr.getClass().copyToCell(ptr, *newCell, pos); + mWorldScene->addObjectToScene(newPtr); + + std::string script = newPtr.getClass().getScript(newPtr); + if (!script.empty()) { + mLocalScripts.add(script, newPtr); + } + addContainerScripts(newPtr, newCell); + } + else if (!newCellActive && currCellActive) { mWorldScene->removeObjectFromScene(ptr); mLocalScripts.remove(ptr); @@ -980,7 +1131,9 @@ namespace MWWorld .copyToCell(ptr, *newCell); newPtr.getRefData().setBaseNode(0); } - else + else if (!currCellActive && !newCellActive) + ptr.getClass().copyToCell(ptr, *newCell); + else // both cells active { MWWorld::Ptr copy = ptr.getClass().copyToCell(ptr, *newCell, pos); @@ -1009,6 +1162,10 @@ namespace MWWorld mRendering->moveObject(ptr, vec); mPhysics->moveObject (ptr); } + if (isPlayer) + { + mWorldScene->playerMoved (vec); + } } bool World::moveObjectImp(const Ptr& ptr, float x, float y, float z) @@ -1048,7 +1205,8 @@ namespace MWWorld const float two_pi = Ogre::Math::TWO_PI; const float pi = Ogre::Math::PI; - float *objRot = ptr.getRefData().getPosition().rot; + ESM::Position pos = ptr.getRefData().getPosition(); + float *objRot = pos.rot; if(adjust) { objRot[0] += rot.x; @@ -1085,55 +1243,39 @@ namespace MWWorld while(objRot[2] < -pi) objRot[2] += two_pi; while(objRot[2] > pi) objRot[2] -= two_pi; - if(ptr.getRefData().getBaseNode() != 0) - { - mRendering->rotateObject(ptr); - mPhysics->rotateObject(ptr); - } + ptr.getRefData().setPosition(pos); + + if(ptr.getRefData().getBaseNode() == 0) + return; + + if (ptr.getClass().isActor()) + mWorldScene->updateObjectRotation(ptr); + else + mWorldScene->updateObjectLocalRotation(ptr); } void World::localRotateObject (const Ptr& ptr, float x, float y, float z) { - if (ptr.getRefData().getBaseNode() != 0) { - - ptr.getRefData().getLocalRotation().rot[0]=Ogre::Degree(x).valueRadians(); - ptr.getRefData().getLocalRotation().rot[1]=Ogre::Degree(y).valueRadians(); - ptr.getRefData().getLocalRotation().rot[2]=Ogre::Degree(z).valueRadians(); - - float fullRotateRad=Ogre::Degree(360).valueRadians(); + LocalRotation rot = ptr.getRefData().getLocalRotation(); + rot.rot[0]=Ogre::Degree(x).valueRadians(); + rot.rot[1]=Ogre::Degree(y).valueRadians(); + rot.rot[2]=Ogre::Degree(z).valueRadians(); - while(ptr.getRefData().getLocalRotation().rot[0]>=fullRotateRad) - ptr.getRefData().getLocalRotation().rot[0]-=fullRotateRad; - while(ptr.getRefData().getLocalRotation().rot[1]>=fullRotateRad) - ptr.getRefData().getLocalRotation().rot[1]-=fullRotateRad; - while(ptr.getRefData().getLocalRotation().rot[2]>=fullRotateRad) - ptr.getRefData().getLocalRotation().rot[2]-=fullRotateRad; + wrap(rot.rot[0]); + wrap(rot.rot[1]); + wrap(rot.rot[2]); - while(ptr.getRefData().getLocalRotation().rot[0]<=-fullRotateRad) - ptr.getRefData().getLocalRotation().rot[0]+=fullRotateRad; - while(ptr.getRefData().getLocalRotation().rot[1]<=-fullRotateRad) - ptr.getRefData().getLocalRotation().rot[1]+=fullRotateRad; - while(ptr.getRefData().getLocalRotation().rot[2]<=-fullRotateRad) - ptr.getRefData().getLocalRotation().rot[2]+=fullRotateRad; + ptr.getRefData().setLocalRotation(rot); - Ogre::Quaternion worldRotQuat(Ogre::Radian(ptr.getRefData().getPosition().rot[2]), Ogre::Vector3::NEGATIVE_UNIT_Z); - if (!ptr.getClass().isActor()) - worldRotQuat = Ogre::Quaternion(Ogre::Radian(ptr.getRefData().getPosition().rot[0]), Ogre::Vector3::NEGATIVE_UNIT_X)* - Ogre::Quaternion(Ogre::Radian(ptr.getRefData().getPosition().rot[1]), Ogre::Vector3::NEGATIVE_UNIT_Y)* worldRotQuat; - - Ogre::Quaternion rot(Ogre::Degree(z), Ogre::Vector3::NEGATIVE_UNIT_Z); - if (!ptr.getClass().isActor()) - rot = Ogre::Quaternion(Ogre::Degree(x), Ogre::Vector3::NEGATIVE_UNIT_X)* - Ogre::Quaternion(Ogre::Degree(y), Ogre::Vector3::NEGATIVE_UNIT_Y)*rot; - - ptr.getRefData().getBaseNode()->setOrientation(worldRotQuat*rot); - mPhysics->rotateObject(ptr); + if (ptr.getRefData().getBaseNode() != 0) + { + mWorldScene->updateObjectLocalRotation(ptr); } } - void World::adjustPosition(const Ptr &ptr) + void World::adjustPosition(const Ptr &ptr, bool force) { - Ogre::Vector3 pos (ptr.getRefData().getPosition().pos); + ESM::Position pos (ptr.getRefData().getPosition()); if(!ptr.getRefData().getBaseNode()) { @@ -1141,21 +1283,34 @@ namespace MWWorld return; } - float terrainHeight = mRendering->getTerrainHeightAt(pos); + float terrainHeight = mRendering->getTerrainHeightAt(Ogre::Vector3(pos.pos)); - if (pos.z < terrainHeight) - pos.z = terrainHeight; + if (pos.pos[2] < terrainHeight) + pos.pos[2] = terrainHeight; - ptr.getRefData().getPosition().pos[2] = pos.z + 20; // place slightly above. will snap down to ground with code below + pos.pos[2] += 20; // place slightly above. will snap down to ground with code below - if (!isFlying(ptr)) + ptr.getRefData().setPosition(pos); + + if (force || !isFlying(ptr)) { - Ogre::Vector3 traced = mPhysics->traceDown(ptr); - if (traced.z < pos.z) - pos.z = traced.z; + Ogre::Vector3 traced = mPhysics->traceDown(ptr, 500); + if (traced.z < pos.pos[2]) + pos.pos[2] = traced.z; } - moveObject(ptr, ptr.getCell(), pos.x, pos.y, pos.z); + moveObject(ptr, ptr.getCell(), pos.pos[0], pos.pos[1], pos.pos[2]); + } + + void World::fixPosition(const Ptr &actor) + { + const float dist = 8000; + ESM::Position pos (actor.getRefData().getPosition()); + pos.pos[2] += dist; + actor.getRefData().setPosition(pos); + + Ogre::Vector3 traced = mPhysics->traceDown(actor, dist*1.1); + moveObject(actor, actor.getCell(), traced.x, traced.y, traced.z); } void World::rotateObject (const Ptr& ptr,float x,float y,float z, bool adjust) @@ -1200,13 +1355,15 @@ namespace MWWorld void World::doPhysics(float duration) { + mPhysics->stepSimulation(duration); + processDoors(duration); mProjectileManager->update(duration); const PtrVelocityList &results = mPhysics->applyQueuedMovement(duration); PtrVelocityList::const_iterator player(results.end()); - for(PtrVelocityList::const_iterator iter(results.begin());iter != results.end();iter++) + for(PtrVelocityList::const_iterator iter(results.begin());iter != results.end();++iter) { if(iter->first.getRefData().getHandle() == "player") { @@ -1218,8 +1375,6 @@ namespace MWWorld } if(player != results.end()) moveObjectImp(player->first, player->second.x, player->second.y, player->second.z); - - mPhysEngine->stepSimulation(duration); } bool World::castRay (float x1, float y1, float z1, float x2, float y2, float z2) @@ -1251,7 +1406,8 @@ namespace MWWorld bool reached = (targetRot == 90.f && it->second) || targetRot == 0.f; /// \todo should use convexSweepTest here - std::vector collisions = mPhysics->getCollisions(it->first); + std::vector collisions = mPhysics->getCollisions(it->first, OEngine::Physic::CollisionType_Actor + , OEngine::Physic::CollisionType_Actor); for (std::vector::iterator cit = collisions.begin(); cit != collisions.end(); ++cit) { MWWorld::Ptr ptr = getPtrViaHandle(*cit); @@ -1267,7 +1423,6 @@ namespace MWWorld // we need to undo the rotation localRotateObject(it->first, 0, 0, oldRot); reached = false; - //break; //Removed in case multiple actors are touching } } @@ -1375,7 +1530,7 @@ namespace MWWorld if (mGoToJail && !paused) goToJail(); - updateWeather(duration); + updateWeather(duration, paused); if (!paused) doPhysics (duration); @@ -1421,68 +1576,42 @@ namespace MWWorld // cast a ray from player to sun to detect if the sun is visible // this is temporary until we find a better place to put this code // currently its here because we need to access the physics system - float* p = mPlayer->getPlayer().getRefData().getPosition().pos; + const float* p = mPlayer->getPlayer().getRefData().getPosition().pos; Vector3 sun = mRendering->getSkyManager()->getRealSunPos(); mRendering->getSkyManager()->setGlare(!mPhysics->castRay(Ogre::Vector3(p[0], p[1], p[2]), sun)); } - - updateFacedHandle (); } - void World::updateFacedHandle () + void World::getFacedHandle(std::string& facedHandle, float maxDistance, bool ignorePlayer) { - float telekinesisRangeBonus = - mPlayer->getPlayer().getClass().getCreatureStats(mPlayer->getPlayer()).getMagicEffects() - .get(ESM::MagicEffect::Telekinesis).mMagnitude; - telekinesisRangeBonus = feetToGameUnits(telekinesisRangeBonus); - - float activationDistance = getMaxActivationDistance() + telekinesisRangeBonus; - activationDistance += mRendering->getCameraDistance(); + maxDistance += mRendering->getCameraDistance(); - // send new query - // figure out which object we want to test against std::vector < std::pair < float, std::string > > results; if (MWBase::Environment::get().getWindowManager()->isGuiMode()) { float x, y; MWBase::Environment::get().getWindowManager()->getMousePosition(x, y); - results = mPhysics->getFacedHandles(x, y, activationDistance); - if (MWBase::Environment::get().getWindowManager()->isConsoleMode()) - results = mPhysics->getFacedHandles(x, y, getMaxActivationDistance ()*50); + results = mPhysics->getFacedHandles(x, y, maxDistance); } else { - results = mPhysics->getFacedHandles(activationDistance); + results = mPhysics->getFacedHandles(maxDistance); } - // ignore the player and other things we're not interested in - std::vector < std::pair < float, std::string > >::iterator it = results.begin(); - while (it != results.end()) - { - if ( (*it).second.find("HeightField") != std::string::npos // not interested in terrain - || getPtrViaHandle((*it).second) == mPlayer->getPlayer() ) // not interested in player (unless you want to talk to yourself) - { - it = results.erase(it); - } - else - ++it; - } + if (ignorePlayer && + !results.empty() && results.front().second == "player") + results.erase(results.begin()); - if (results.empty()) - { - mFacedHandle = ""; - mFacedDistance = FLT_MAX; - } + if (results.empty() + || results.front().second.find("HeightField") != std::string::npos) // Blocked by terrain + facedHandle = ""; else - { - mFacedHandle = results.front().second; - mFacedDistance = results.front().first; - } + facedHandle = results.front().second; } bool World::isCellExterior() const { - CellStore *currentCell = mWorldScene->getCurrentCell(); + const CellStore *currentCell = mWorldScene->getCurrentCell(); if (currentCell) { return currentCell->getCell()->isExterior(); @@ -1492,7 +1621,7 @@ namespace MWWorld bool World::isCellQuasiExterior() const { - CellStore *currentCell = mWorldScene->getCurrentCell(); + const CellStore *currentCell = mWorldScene->getCurrentCell(); if (currentCell) { if (!(currentCell->getCell()->mData.mFlags & ESM::Cell::QuasiEx)) @@ -1518,11 +1647,6 @@ namespace MWWorld mWeatherManager->modRegion(regionid, chances); } - OEngine::Render::Fader* World::getFader() - { - return mRendering->getFader(); - } - Ogre::Vector2 World::getNorthVector (CellStore* cell) { MWWorld::CellRefList& statics = cell->get(); @@ -1560,9 +1684,14 @@ namespace MWWorld } } - void World::getInteriorMapPosition (Ogre::Vector2 position, float& nX, float& nY, int &x, int& y) + void World::worldToInteriorMapPosition (Ogre::Vector2 position, float& nX, float& nY, int &x, int& y) + { + mRendering->worldToInteriorMapPosition(position, nX, nY, x, y); + } + + Ogre::Vector2 World::interiorMapToWorldPosition(float nX, float nY, int x, int y) { - mRendering->getInteriorMapPosition(position, nX, nY, x, y); + return mRendering->interiorMapToWorldPosition(nX, nY, x, y); } bool World::isPositionExplored (float nX, float nY, int x, int y, bool interior) @@ -1572,6 +1701,7 @@ namespace MWWorld void World::setWaterHeight(const float height) { + mPhysics->setWaterHeight(height); mRendering->setWaterHeight(height); } @@ -1580,6 +1710,11 @@ namespace MWWorld return mRendering->toggleWater(); } + bool World::toggleWorld() + { + return mRendering->toggleWorld(); + } + void World::PCDropped (const Ptr& item) { std::string script = item.getClass().getScript(item); @@ -1621,12 +1756,20 @@ namespace MWWorld bool World::canPlaceObject(float cursorX, float cursorY) { Ogre::Vector3 normal(0,0,0); - std::pair result = mPhysics->castRay(cursorX, cursorY, &normal); + std::string handle; + std::pair result = mPhysics->castRay(cursorX, cursorY, &normal, &handle); if (result.first) { // check if the wanted position is on a flat surface, and not e.g. against a vertical wall - return (normal.angleBetween(Ogre::Vector3(0.f,0.f,1.f)).valueDegrees() < 30); + if (normal.angleBetween(Ogre::Vector3(0.f,0.f,1.f)).valueDegrees() >= 30) + return false; + + MWWorld::Ptr hitObject = searchPtrViaHandle(handle); + if (!hitObject.isEmpty() && hitObject.getClass().isActor()) + return false; + + return true; } else return false; @@ -1666,6 +1809,15 @@ namespace MWWorld MWWorld::Ptr dropped = object.getClass().copyToCell(object, *cell, pos); + // Reset some position values that could be uninitialized if this item came from a container + LocalRotation localRotation; + localRotation.rot[0] = 0; + localRotation.rot[1] = 0; + localRotation.rot[2] = 0; + dropped.getRefData().setLocalRotation(localRotation); + dropped.getCellRef().setPosition(pos); + dropped.getCellRef().unsetRefNum(); + if (mWorldScene->isCellActive(*cell)) { if (dropped.getRefData().isEnabled()) { mWorldScene->addObjectToScene(dropped); @@ -1692,14 +1844,15 @@ namespace MWWorld Ogre::Vector3 orig = Ogre::Vector3(pos.pos); + orig.z += 20; Ogre::Vector3 dir = Ogre::Vector3(0, 0, -1); - float len = (pos.pos[2] >= 0) ? pos.pos[2] : -pos.pos[2]; - len += 100.0; + float len = 100.0; std::pair hit = mPhysics->castRay(orig, dir, len); - pos.pos[2] = hit.second.z; + if (hit.first) + pos.pos[2] = hit.second.z; // copy the object and set its count int origCount = object.getRefData().getCount(); @@ -1725,6 +1878,9 @@ namespace MWWorld bool World::isFlying(const MWWorld::Ptr &ptr) const { + const MWMechanics::CreatureStats &stats = ptr.getClass().getCreatureStats(ptr); + bool isParalyzed = (stats.getMagicEffects().get(ESM::MagicEffect::Paralyze).getMagnitude() > 0); + if(!ptr.getClass().isActor()) return false; @@ -1732,10 +1888,9 @@ namespace MWWorld return false; if (ptr.getClass().canFly(ptr)) - return true; + return !isParalyzed; - const MWMechanics::CreatureStats &stats = ptr.getClass().getCreatureStats(ptr); - if(stats.getMagicEffects().get(ESM::MagicEffect::Levitate).mMagnitude > 0 + if(stats.getMagicEffects().get(ESM::MagicEffect::Levitate).getMagnitude() > 0 && isLevitationEnabled()) return true; @@ -1753,7 +1908,7 @@ namespace MWWorld return false; const MWMechanics::CreatureStats &stats = ptr.getClass().getCreatureStats(ptr); - if(stats.getMagicEffects().get(ESM::MagicEffect::SlowFall).mMagnitude > 0) + if(stats.getMagicEffects().get(ESM::MagicEffect::SlowFall).getMagnitude() > 0) return true; return false; @@ -1761,7 +1916,7 @@ namespace MWWorld bool World::isSubmerged(const MWWorld::Ptr &object) const { - float *fpos = object.getRefData().getPosition().pos; + const float *fpos = object.getRefData().getPosition().pos; Ogre::Vector3 pos(fpos[0], fpos[1], fpos[2]); const OEngine::Physic::PhysicActor *actor = mPhysEngine->getCharacter(object.getRefData().getHandle()); @@ -1774,7 +1929,7 @@ namespace MWWorld World::isSwimming(const MWWorld::Ptr &object) const { /// \todo add check ifActor() - only actors can swim - float *fpos = object.getRefData().getPosition().pos; + const float *fpos = object.getRefData().getPosition().pos; Ogre::Vector3 pos(fpos[0], fpos[1], fpos[2]); /// \fixme 3/4ths submerged? @@ -1818,7 +1973,7 @@ namespace MWWorld Ogre::Vector3 pos(ptr.getRefData().getPosition().pos); OEngine::Physic::ActorTracer tracer; // a small distance above collision object is considered "on ground" - tracer.findGround(physactor->getCollisionBody(), + tracer.findGround(physactor, pos, pos - Ogre::Vector3(0, 0, 1.5f), // trace a small amount down mPhysEngine); @@ -1848,7 +2003,12 @@ namespace MWWorld if (!mPlayer) mPlayer = new MWWorld::Player(player, *this); else + { + // Remove the old CharacterController + MWBase::Environment::get().getMechanicsManager()->remove(getPlayerPtr()); + mPlayer->set(player); + } Ptr ptr = mPlayer->getPlayer(); mRendering->setupPlayer(ptr); @@ -1876,7 +2036,7 @@ namespace MWWorld Ogre::Vector3 playerPos(refdata.getPosition().pos); const OEngine::Physic::PhysicActor *physactor = mPhysEngine->getCharacter(refdata.getHandle()); - if((!physactor->getOnGround()&&physactor->getCollisionMode()) || isUnderwater(currentCell, playerPos)) + if((!physactor->getOnGround()&&physactor->getCollisionMode()) || isUnderwater(currentCell, playerPos) || isWalkingOnWater(player)) return 2; if((currentCell->getCell()->mData.mFlags&ESM::Cell::NoSleep) || player.getClass().getNpcStats(player).isWerewolf()) @@ -1923,27 +2083,100 @@ namespace MWWorld mDoorStates[door] = state; } - void World::activateDoor(const Ptr &door, bool open) + void World::activateDoor(const Ptr &door, int state) { - int state = open ? 1 : 2; door.getClass().setDoorState(door, state); mDoorStates[door] = state; + if (state == 0) + mDoorStates.erase(door); } bool World::getPlayerStandingOn (const MWWorld::Ptr& object) { - MWWorld::Ptr player = mPlayer->getPlayer(); - if (!mPhysEngine->getCharacter("player")->getOnGround()) - return false; - btVector3 from (player.getRefData().getPosition().pos[0], player.getRefData().getPosition().pos[1], player.getRefData().getPosition().pos[2]); - btVector3 to = from - btVector3(0,0,5); - std::pair result = mPhysEngine->rayTest(from, to); - return result.first == object.getRefData().getBaseNode()->getName(); + MWWorld::Ptr player = getPlayerPtr(); + return mPhysics->isActorStandingOn(player, object); } bool World::getActorStandingOn (const MWWorld::Ptr& object) { - return mPhysEngine->isAnyActorStandingOn(object.getRefData().getBaseNode()->getName()); + std::vector actors; + mPhysics->getActorsStandingOn(object, actors); + return !actors.empty(); + } + + bool World::getPlayerCollidingWith (const MWWorld::Ptr& object) + { + MWWorld::Ptr player = getPlayerPtr(); + return mPhysics->isActorCollidingWith(player, object); + } + + bool World::getActorCollidingWith (const MWWorld::Ptr& object) + { + std::vector actors; + mPhysics->getActorsCollidingWith(object, actors); + return !actors.empty(); + } + + void World::hurtStandingActors(const Ptr &object, float healthPerSecond) + { + if (MWBase::Environment::get().getWindowManager()->isGuiMode()) + return; + + std::vector actors; + mPhysics->getActorsStandingOn(object, actors); + for (std::vector::iterator it = actors.begin(); it != actors.end(); ++it) + { + MWWorld::Ptr actor = searchPtrViaHandle(*it); // Collision events are from the last frame, actor might no longer exist + if (actor.isEmpty()) + continue; + + MWMechanics::CreatureStats& stats = actor.getClass().getCreatureStats(actor); + if (stats.isDead()) + continue; + MWMechanics::DynamicStat health = stats.getHealth(); + health.setCurrent(health.getCurrent()-healthPerSecond*MWBase::Environment::get().getFrameDuration()); + stats.setHealth(health); + + if (healthPerSecond > 0.0f) + { + if (actor.getRefData().getHandle() == "player") + MWBase::Environment::get().getWindowManager()->activateHitOverlay(false); + + if (!MWBase::Environment::get().getSoundManager()->getSoundPlaying(actor, "Health Damage")) + MWBase::Environment::get().getSoundManager()->playSound3D(actor, "Health Damage", 1.0f, 1.0f); + } + } + } + + void World::hurtCollidingActors(const Ptr &object, float healthPerSecond) + { + if (MWBase::Environment::get().getWindowManager()->isGuiMode()) + return; + + std::vector actors; + mPhysics->getActorsCollidingWith(object, actors); + for (std::vector::iterator it = actors.begin(); it != actors.end(); ++it) + { + MWWorld::Ptr actor = searchPtrViaHandle(*it); // Collision events are from the last frame, actor might no longer exist + if (actor.isEmpty()) + continue; + + MWMechanics::CreatureStats& stats = actor.getClass().getCreatureStats(actor); + if (stats.isDead()) + continue; + MWMechanics::DynamicStat health = stats.getHealth(); + health.setCurrent(health.getCurrent()-healthPerSecond*MWBase::Environment::get().getFrameDuration()); + stats.setHealth(health); + + if (healthPerSecond > 0.0f) + { + if (actor.getRefData().getHandle() == "player") + MWBase::Environment::get().getWindowManager()->activateHitOverlay(false); + + if (!MWBase::Environment::get().getSoundManager()->getSoundPlaying(actor, "Health Damage")) + MWBase::Environment::get().getSoundManager()->playSound3D(actor, "Health Damage", 1.0f, 1.0f); + } + } } float World::getWindSpeed() @@ -1954,6 +2187,22 @@ namespace MWWorld return 0.f; } + bool World::isInStorm() const + { + if (isCellExterior() || isCellQuasiExterior()) + return mWeatherManager->isInStorm(); + else + return false; + } + + Ogre::Vector3 World::getStormDirection() const + { + if (isCellExterior() || isCellQuasiExterior()) + return mWeatherManager->getStormDirection(); + else + return Ogre::Vector3(0,1,0); + } + void World::getContainersOwnedBy (const MWWorld::Ptr& npc, std::vector& out) { const Scene::CellStoreCollection& collection = mWorldScene->getActiveCells(); @@ -1997,20 +2246,24 @@ namespace MWWorld } } - bool World::getLOS(const MWWorld::Ptr& npc,const MWWorld::Ptr& targetNpc) + bool World::getLOS(const MWWorld::Ptr& actor,const MWWorld::Ptr& targetActor) { - if (!targetNpc.getRefData().isEnabled() || !npc.getRefData().isEnabled()) + if (!targetActor.getRefData().isEnabled() || !actor.getRefData().isEnabled()) return false; // cannot get LOS unless both NPC's are enabled - Ogre::Vector3 halfExt1 = mPhysEngine->getCharacter(npc.getRefData().getHandle())->getHalfExtents(); - float* pos1 = npc.getRefData().getPosition().pos; - Ogre::Vector3 halfExt2 = mPhysEngine->getCharacter(targetNpc.getRefData().getHandle())->getHalfExtents(); - float* pos2 = targetNpc.getRefData().getPosition().pos; + if (!targetActor.getRefData().getBaseNode() || !targetActor.getRefData().getBaseNode()) + return false; // not in active cell - btVector3 from(pos1[0],pos1[1],pos1[2]+halfExt1.z); - btVector3 to(pos2[0],pos2[1],pos2[2]+halfExt2.z); + Ogre::Vector3 halfExt1 = mPhysEngine->getCharacter(actor.getRefData().getHandle())->getHalfExtents(); + const float* pos1 = actor.getRefData().getPosition().pos; + Ogre::Vector3 halfExt2 = mPhysEngine->getCharacter(targetActor.getRefData().getHandle())->getHalfExtents(); + const float* pos2 = targetActor.getRefData().getPosition().pos; + + btVector3 from(pos1[0],pos1[1],pos1[2]+halfExt1.z*2*0.9); // eye level + btVector3 to(pos2[0],pos2[1],pos2[2]+halfExt2.z*2*0.9); std::pair result = mPhysEngine->rayTest(from, to,false); if(result.first == "") return true; + return false; } @@ -2030,8 +2283,8 @@ namespace MWWorld void World::enableActorCollision(const MWWorld::Ptr& actor, bool enable) { OEngine::Physic::PhysicActor *physicActor = mPhysEngine->getCharacter(actor.getRefData().getHandle()); - - physicActor->enableCollisionBody(enable); + if (physicActor) + physicActor->enableCollisionBody(enable); } bool World::findInteriorPosition(const std::string &name, ESM::Position &pos) @@ -2057,8 +2310,8 @@ namespace MWWorld // door to exterior if (it->mRef.getDestCell().empty()) { int x, y; - const float *pos = it->mRef.getDoorDest().pos; - positionToIndex(pos[0], pos[1], x, y); + ESM::Position doorDest = it->mRef.getDoorDest(); + positionToIndex(doorDest.pos[0], doorDest.pos[1], x, y); source = getExterior(x, y); } // door to interior @@ -2168,6 +2421,36 @@ namespace MWWorld windowManager->unsetForceHide(MWGui::GW_Inventory); windowManager->unsetForceHide(MWGui::GW_Magic); } + + windowManager->setWerewolfOverlay(werewolf); + + // Witnesses of the player's transformation will make them a globally known werewolf + std::vector closeActors; + MWBase::Environment::get().getMechanicsManager()->getActorsInRange(Ogre::Vector3(actor.getRefData().getPosition().pos), + getStore().get().search("fAlarmRadius")->getFloat(), + closeActors); + + bool detected = false; + for (std::vector::const_iterator it = closeActors.begin(); it != closeActors.end(); ++it) + { + if (*it == actor) + continue; + + if (!it->getClass().isNpc()) + continue; + + if (getLOS(*it, actor) && MWBase::Environment::get().getMechanicsManager()->awarenessCheck(actor, *it)) + { + detected = true; + break; + } + } + + if (detected) + { + windowManager->messageBox("#{sWerewolfAlarmMessage}"); + setGlobalInt("pcknownwerewolf", 1); + } } } @@ -2236,15 +2519,10 @@ namespace MWWorld } // If this is a power, check if it was already used in the last 24h - if (!fail && spell->mData.mType == ESM::Spell::ST_Power) + if (!fail && spell->mData.mType == ESM::Spell::ST_Power && !stats.getSpells().canUsePower(spell->mId)) { - if (stats.getSpells().canUsePower(spell->mId)) - stats.getSpells().usePower(spell->mId); - else - { - message = "#{sPowerAlreadyUsed}"; - fail = true; - } + message = "#{sPowerAlreadyUsed}"; + fail = true; } // Reduce mana @@ -2265,7 +2543,49 @@ namespace MWWorld { MWMechanics::CreatureStats& stats = actor.getClass().getCreatureStats(actor); - MWWorld::Ptr target = getFacedObject(); + // Get the target to use for "on touch" effects + MWWorld::Ptr target; + float distance = 192.f; // ?? + + if (actor == getPlayerPtr()) + { + // For the player, use camera to aim + std::string facedHandle; + getFacedHandle(facedHandle, distance); + if (!facedHandle.empty()) + target = getPtrViaHandle(facedHandle); + } + else + { + // For NPCs use facing direction from Head node + Ogre::Vector3 origin(actor.getRefData().getPosition().pos); + MWRender::Animation *anim = mRendering->getAnimation(actor); + if(anim != NULL) + { + Ogre::Node *node = anim->getNode("Head"); + if (node == NULL) + node = anim->getNode("Bip01 Head"); + if(node != NULL) + origin += node->_getDerivedPosition(); + } + Ogre::Quaternion orient; + orient = Ogre::Quaternion(Ogre::Radian(actor.getRefData().getPosition().rot[2]), Ogre::Vector3::NEGATIVE_UNIT_Z) * + Ogre::Quaternion(Ogre::Radian(actor.getRefData().getPosition().rot[0]), Ogre::Vector3::NEGATIVE_UNIT_X); + Ogre::Vector3 direction = orient.yAxis(); + Ogre::Vector3 dest = origin + direction * distance; + + + std::vector > collisions = mPhysEngine->rayTest2(btVector3(origin.x, origin.y, origin.z), btVector3(dest.x, dest.y, dest.z)); + for (std::vector >::iterator cIt = collisions.begin(); cIt != collisions.end(); ++cIt) + { + MWWorld::Ptr collided = getPtrViaHandle(cIt->second); + if (collided != actor) + { + target = collided; + break; + } + } + } std::string selectedSpell = stats.getSpells().getSelectedSpell(); @@ -2277,6 +2597,10 @@ namespace MWWorld { const ESM::Spell* spell = getStore().get().search(selectedSpell); + // A power can be used once per 24h + if (spell->mData.mType == ESM::Spell::ST_Power) + stats.getSpells().usePower(spell->mId); + cast.cast(spell); } else if (actor.getClass().hasInventoryStore(actor)) @@ -2295,9 +2619,9 @@ namespace MWWorld void World::launchMagicBolt (const std::string& model, const std::string &sound, const std::string &spellId, float speed, bool stack, const ESM::EffectList& effects, - const MWWorld::Ptr& actor, const std::string& sourceName) + const MWWorld::Ptr& caster, const std::string& sourceName, const Ogre::Vector3& fallbackDirection) { - mProjectileManager->launchMagicBolt(model, sound, spellId, speed, stack, effects, actor, sourceName); + mProjectileManager->launchMagicBolt(model, sound, spellId, speed, stack, effects, caster, sourceName, fallbackDirection); } const std::vector& World::getContentFiles() const @@ -2310,6 +2634,9 @@ namespace MWWorld actor.getClass().getCreatureStats(actor).getActiveSpells().purgeEffect(ESM::MagicEffect::Invisibility); if (actor.getClass().hasInventoryStore(actor)) actor.getClass().getInventoryStore(actor).purgeEffect(ESM::MagicEffect::Invisibility); + + // Normally updated once per frame, but here it is kinda important to do it right away. + MWBase::Environment::get().getMechanicsManager()->updateMagicEffects(actor); } bool World::isDark() const @@ -2379,16 +2706,16 @@ namespace MWWorld MWWorld::ActionTeleport action("", closestMarker.getRefData().getPosition()); action.execute(ptr); } - - void World::updateWeather(float duration) + + void World::updateWeather(float duration, bool paused) { if (mPlayer->wasTeleported()) { mPlayer->setTeleported(false); mWeatherManager->switchToNextWeather(true); } - - mWeatherManager->update(duration); + + mWeatherManager->update(duration, paused); } struct AddDetectedReference @@ -2411,14 +2738,15 @@ namespace MWWorld if (!ptr.getRefData().isEnabled()) return true; - // Consider references inside containers as well - if (ptr.getClass().isActor() || ptr.getClass().getTypeName() == typeid(ESM::Container).name()) + // Consider references inside containers as well (except if we are looking for a Creature, they cannot be in containers) + if (mType != World::Detect_Creature && + (ptr.getClass().isActor() || ptr.getClass().getTypeName() == typeid(ESM::Container).name())) { MWWorld::ContainerStore& store = ptr.getClass().getContainerStore(ptr); { for (MWWorld::ContainerStoreIterator it = store.begin(); it != store.end(); ++it) { - if (needToAdd(*it)) + if (needToAdd(*it, mDetector)) { mOut.push_back(ptr); return true; @@ -2427,16 +2755,25 @@ namespace MWWorld } } - if (needToAdd(ptr)) + if (needToAdd(ptr, mDetector)) mOut.push_back(ptr); return true; } - bool needToAdd (MWWorld::Ptr ptr) + bool needToAdd (MWWorld::Ptr ptr, MWWorld::Ptr detector) { - if (mType == World::Detect_Creature && ptr.getClass().getTypeName() != typeid(ESM::Creature).name()) - return false; + if (mType == World::Detect_Creature) + { + // If in werewolf form, this detects only NPCs, otherwise only creatures + if (detector.getClass().isNpc() && detector.getClass().getNpcStats(detector).isWerewolf()) + { + if (ptr.getClass().getTypeName() != typeid(ESM::NPC).name()) + return false; + } + else if (ptr.getClass().getTypeName() != typeid(ESM::Creature).name()) + return false; + } if (mType == World::Detect_Key && !ptr.getClass().isKey(ptr)) return false; if (mType == World::Detect_Enchantment && ptr.getClass().getEnchantment(ptr).empty()) @@ -2450,11 +2787,11 @@ namespace MWWorld const MWMechanics::MagicEffects& effects = ptr.getClass().getCreatureStats(ptr).getMagicEffects(); float dist=0; if (type == World::Detect_Creature) - dist = effects.get(ESM::MagicEffect::DetectAnimal).mMagnitude; + dist = effects.get(ESM::MagicEffect::DetectAnimal).getMagnitude(); else if (type == World::Detect_Key) - dist = effects.get(ESM::MagicEffect::DetectKey).mMagnitude; + dist = effects.get(ESM::MagicEffect::DetectKey).getMagnitude(); else if (type == World::Detect_Enchantment) - dist = effects.get(ESM::MagicEffect::DetectEnchantment).mMagnitude; + dist = effects.get(ESM::MagicEffect::DetectEnchantment).getMagnitude(); if (!dist) return; @@ -2492,9 +2829,15 @@ namespace MWWorld float fCrimeGoldDiscountMult = getStore().get().find("fCrimeGoldDiscountMult")->getFloat(); float fCrimeGoldTurnInMult = getStore().get().find("fCrimeGoldTurnInMult")->getFloat(); - int discount = bounty*fCrimeGoldDiscountMult; + int discount = bounty * fCrimeGoldDiscountMult; int turnIn = bounty * fCrimeGoldTurnInMult; + if (bounty > 0) + { + discount = std::max(1, discount); + turnIn = std::max(1, turnIn); + } + mGlobalVariables["pchascrimegold"].setInteger((bounty <= playerGold) ? 1 : 0); mGlobalVariables["pchasgolddiscount"].setInteger((discount <= playerGold) ? 1 : 0); @@ -2536,7 +2879,8 @@ namespace MWWorld ContainerStore& store = ptr.getClass().getContainerStore(ptr); for (ContainerStoreIterator it = store.begin(); it != store.end(); ++it) //Move all stolen stuff into chest { - if (!it->getCellRef().getOwner().empty() && it->getCellRef().getOwner() != "player") //Not owned by no one/player? + MWWorld::Ptr dummy; + if (!MWBase::Environment::get().getMechanicsManager()->isAllowedToUse(getPlayerPtr(), *it, dummy)) { closestChest.getClass().getContainerStore(closestChest).add(*it, it->getRefData().getCount(), closestChest); store.remove(*it, it->getRefData().getCount(), ptr); @@ -2550,24 +2894,34 @@ namespace MWWorld { if (!mGoToJail) { - // Save for next update, since the player should be able to read the dialog text first + // Reset bounty and forget the crime now, but don't change cell yet (the player should be able to read the dialog text first) mGoToJail = true; + + MWWorld::Ptr player = getPlayerPtr(); + + int bounty = player.getClass().getNpcStats(player).getBounty(); + player.getClass().getNpcStats(player).setBounty(0); + mPlayer->recordCrimeId(); + confiscateStolenItems(player); + + int iDaysinPrisonMod = getStore().get().find("iDaysinPrisonMod")->getInt(); + mDaysInPrison = std::max(1, bounty / iDaysinPrisonMod); + return; } else { mGoToJail = false; + MWBase::Environment::get().getWindowManager()->removeGuiMode(MWGui::GM_Dialogue); + MWWorld::Ptr player = getPlayerPtr(); teleportToClosestMarker(player, "prisonmarker"); - int bounty = player.getClass().getNpcStats(player).getBounty(); - player.getClass().getNpcStats(player).setBounty(0); - confiscateStolenItems(player); - - int iDaysinPrisonMod = getStore().get().find("iDaysinPrisonMod")->getInt(); - int days = std::max(1, bounty / iDaysinPrisonMod); + int days = mDaysInPrison; advanceTime(days * 24); + for (int i=0; irest (true); std::set skills; for (int day=0; day buttons; buttons.push_back("#{sOk}"); MWBase::Environment::get().getWindowManager()->messageBox(message, buttons); @@ -2654,6 +3006,9 @@ namespace MWWorld void World::spawnBloodEffect(const Ptr &ptr, const Vector3 &worldPosition) { + if (ptr.getRefData().getHandle() == "player" && Settings::Manager::getBool("hit fader", "GUI")) + return; + int type = ptr.getClass().getBloodTexture(ptr); std::string texture; switch (type) @@ -2679,6 +3034,11 @@ namespace MWWorld mRendering->spawnEffect(model, texture, worldPosition); } + void World::spawnEffect(const std::string &model, const std::string &textureOverride, const Vector3 &worldPos) + { + mRendering->spawnEffect(model, textureOverride, worldPos); + } + void World::explodeSpell(const Vector3 &origin, const ESM::EffectList &effects, const Ptr &caster, const std::string& id, const std::string& sourceName) { @@ -2742,4 +3102,57 @@ namespace MWWorld cast.inflict(apply->first, caster, effects, ESM::RT_Target, false, true); } } + + void World::activate(const Ptr &object, const Ptr &actor) + { + MWScript::InterpreterContext interpreterContext (&object.getRefData().getLocals(), object); + interpreterContext.activate (object); + + std::string script = object.getClass().getScript (object); + + breakInvisibility(actor); + + if (!script.empty()) + { + getLocalScripts().setIgnore (object); + MWBase::Environment::get().getScriptManager()->run (script, interpreterContext); + } + if (!interpreterContext.hasActivationBeenHandled()) + interpreterContext.executeActivation(object, actor); + } + + struct ResetActorsFunctor + { + bool operator() (Ptr ptr) + { + // Can't reset actors that were moved to a different cell, because we don't know what cell they came from. + // This could be fixed once we properly track actor cell changes, but may not be desirable behaviour anyhow. + if (ptr.getClass().isActor() && ptr.getCellRef().getRefNum().mContentFile != -1) + { + const ESM::Position& origPos = ptr.getCellRef().getPosition(); + MWBase::Environment::get().getWorld()->moveObject(ptr, origPos.pos[0], origPos.pos[1], origPos.pos[2]); + MWBase::Environment::get().getWorld()->rotateObject(ptr, origPos.rot[0], origPos.rot[1], origPos.rot[2]); + ptr.getClass().adjustPosition(ptr, false); + } + return true; + } + }; + void World::resetActors() + { + for (Scene::CellStoreCollection::const_iterator iter (mWorldScene->getActiveCells().begin()); + iter!=mWorldScene->getActiveCells().end(); ++iter) + { + CellStore* cellstore = *iter; + ResetActorsFunctor functor; + cellstore->forEach(functor); + } + } + + bool World::isWalkingOnWater(const Ptr &actor) + { + OEngine::Physic::PhysicActor* physicActor = mPhysEngine->getCharacter(actor.getRefData().getHandle()); + if (physicActor && physicActor->isWalkingOnWater()) + return true; + return false; + } } diff --git a/apps/openmw/mwworld/worldimp.hpp b/apps/openmw/mwworld/worldimp.hpp index 0a396ef5c..fef279705 100644 --- a/apps/openmw/mwworld/worldimp.hpp +++ b/apps/openmw/mwworld/worldimp.hpp @@ -91,8 +91,6 @@ namespace MWWorld Ptr getPtrViaHandle (const std::string& handle, CellStore& cellStore); int mActivationDistanceOverride; - std::string mFacedHandle; - float mFacedDistance; std::string mStartupScript; @@ -101,7 +99,7 @@ namespace MWWorld std::string mStartCell; - void updateWeather(float duration); + void updateWeather(float duration, bool paused = false); int getDaysPerMonth (int month) const; void rotateObjectImp (const Ptr& ptr, Ogre::Vector3 rot, bool adjust); @@ -113,7 +111,7 @@ namespace MWWorld void updateWindowManager (); void performUpdateSceneQueries (); - void updateFacedHandle (); + void getFacedHandle(std::string& facedHandle, float maxDistance, bool ignorePlayer=true); float getMaxActivationDistance (); float getNpcActivationDistance (); @@ -143,6 +141,7 @@ namespace MWWorld bool mTeleportEnabled; bool mLevitationEnabled; bool mGoToJail; + int mDaysInPrison; float feetToGameUnits(float feet); @@ -169,9 +168,6 @@ namespace MWWorld virtual void readRecord (ESM::ESMReader& reader, int32_t type, const std::map& contentFileMap); - virtual OEngine::Render::Fader* getFader(); - ///< \todo remove this function. Rendering details should not be exposed. - virtual CellStore *getExterior (int x, int y); virtual CellStore *getInterior (const std::string& name); @@ -184,6 +180,7 @@ namespace MWWorld virtual void setWaterHeight(const float height); virtual bool toggleWater(); + virtual bool toggleWorld(); virtual void adjustSky(); @@ -201,20 +198,23 @@ namespace MWWorld virtual LocalScripts& getLocalScripts(); virtual bool hasCellChanged() const; - ///< Has the player moved to a different cell, since the last frame? + ///< Has the set of active cells changed, since the last frame? virtual bool isCellExterior() const; virtual bool isCellQuasiExterior() const; virtual Ogre::Vector2 getNorthVector (CellStore* cell); - ///< get north vector (OGRE coordinates) for given interior cell + ///< get north vector for given interior cell virtual void getDoorMarkers (MWWorld::CellStore* cell, std::vector& out); ///< get a list of teleport door markers for a given cell, to be displayed on the local map - virtual void getInteriorMapPosition (Ogre::Vector2 position, float& nX, float& nY, int &x, int& y); - ///< see MWRender::LocalMap::getInteriorMapPosition + virtual void worldToInteriorMapPosition (Ogre::Vector2 position, float& nX, float& nY, int &x, int& y); + ///< see MWRender::LocalMap::worldToInteriorMapPosition + + virtual Ogre::Vector2 interiorMapToWorldPosition (float nX, float nY, int x, int y); + ///< see MWRender::LocalMap::interiorMapToWorldPosition virtual bool isPositionExplored (float nX, float nY, int x, int y, bool interior); ///< see MWRender::LocalMap::isPositionExplored @@ -260,8 +260,16 @@ namespace MWWorld virtual Ptr searchPtrViaActorId (int actorId); ///< Search is limited to the active cells. - virtual void adjustPosition (const Ptr& ptr); + virtual MWWorld::Ptr findContainer (const MWWorld::Ptr& ptr); + ///< Return a pointer to a liveCellRef which contains \a ptr. + /// \note Search is limited to the active cells. + + virtual void adjustPosition (const Ptr& ptr, bool force); ///< Adjust position after load to be on ground. Must be called after model load. + /// @param force do this even if the ptr is flying + + virtual void fixPosition (const Ptr& actor); + ///< Attempt to fix position so that the Ptr is no longer inside collision geometry. virtual void enable (const Ptr& ptr); @@ -336,10 +344,11 @@ namespace MWWorld virtual void scaleObject (const Ptr& ptr, float scale); - /// Rotates object, uses degrees + /// World rotates object, uses degrees /// \param adjust indicates rotation should be set or adjusted virtual void rotateObject (const Ptr& ptr,float x,float y,float z, bool adjust = false); + /// Local rotates object, uses degrees virtual void localRotateObject (const Ptr& ptr, float x, float y, float z); virtual MWWorld::Ptr safePlaceObject(const MWWorld::Ptr& ptr, MWWorld::CellStore* cell, ESM::Position pos); @@ -469,11 +478,22 @@ namespace MWWorld /// open or close a non-teleport door (depending on current state) virtual void activateDoor(const MWWorld::Ptr& door); - /// open or close a non-teleport door as specified - virtual void activateDoor(const MWWorld::Ptr& door, bool open); + /// update movement state of a non-teleport door as specified + /// @param state see MWClass::setDoorState + /// @note throws an exception when invoked on a teleport door + virtual void activateDoor(const MWWorld::Ptr& door, int state); virtual bool getPlayerStandingOn (const MWWorld::Ptr& object); ///< @return true if the player is standing on \a object virtual bool getActorStandingOn (const MWWorld::Ptr& object); ///< @return true if any actor is standing on \a object + virtual bool getPlayerCollidingWith(const MWWorld::Ptr& object); ///< @return true if the player is colliding with \a object + virtual bool getActorCollidingWith (const MWWorld::Ptr& object); ///< @return true if any actor is colliding with \a object + virtual void hurtStandingActors (const MWWorld::Ptr& object, float dmgPerSecond); + ///< Apply a health difference to any actors standing on \a object. + /// To hurt actors, healthPerSecond should be a positive value. For a negative value, actors will be healed. + virtual void hurtCollidingActors (const MWWorld::Ptr& object, float dmgPerSecond); + ///< Apply a health difference to any actors colliding with \a object. + /// To hurt actors, healthPerSecond should be a positive value. For a negative value, actors will be healed. + virtual float getWindSpeed(); virtual void getContainersOwnedBy (const MWWorld::Ptr& npc, std::vector& out); @@ -481,7 +501,7 @@ namespace MWWorld virtual void getItemsOwnedBy (const MWWorld::Ptr& npc, std::vector& out); ///< get all items in active cells owned by this Npc - virtual bool getLOS(const MWWorld::Ptr& npc,const MWWorld::Ptr& targetNpc); + virtual bool getLOS(const MWWorld::Ptr& actor,const MWWorld::Ptr& targetActor); ///< get Line of Sight (morrowind stupid implementation) virtual float getDistToNearestRayHit(const Ogre::Vector3& from, const Ogre::Vector3& dir, float maxDist); @@ -545,7 +565,7 @@ namespace MWWorld virtual void launchMagicBolt (const std::string& model, const std::string& sound, const std::string& spellId, float speed, bool stack, const ESM::EffectList& effects, - const MWWorld::Ptr& actor, const std::string& sourceName); + const MWWorld::Ptr& caster, const std::string& sourceName, const Ogre::Vector3& fallbackDirection); virtual void launchProjectile (MWWorld::Ptr actor, MWWorld::Ptr projectile, const Ogre::Vector3& worldPos, const Ogre::Quaternion& orient, MWWorld::Ptr bow, float speed); @@ -584,8 +604,23 @@ namespace MWWorld /// Spawn a blood effect for \a ptr at \a worldPosition virtual void spawnBloodEffect (const MWWorld::Ptr& ptr, const Ogre::Vector3& worldPosition); + virtual void spawnEffect (const std::string& model, const std::string& textureOverride, const Ogre::Vector3& worldPos); + virtual void explodeSpell (const Ogre::Vector3& origin, const ESM::EffectList& effects, const MWWorld::Ptr& caster, const std::string& id, const std::string& sourceName); + + virtual void activate (const MWWorld::Ptr& object, const MWWorld::Ptr& actor); + + /// @see MWWorld::WeatherManager::isInStorm + virtual bool isInStorm() const; + + /// @see MWWorld::WeatherManager::getStormDirection + virtual Ogre::Vector3 getStormDirection() const; + + /// Resets all actors in the current active cells to their original location within that cell. + virtual void resetActors(); + + virtual bool isWalkingOnWater (const MWWorld::Ptr& actor); }; } diff --git a/apps/openmw_test_suite/components/misc/test_stringops.cpp b/apps/openmw_test_suite/components/misc/test_stringops.cpp index 44587c445..55fe0e0c2 100644 --- a/apps/openmw_test_suite/components/misc/test_stringops.cpp +++ b/apps/openmw_test_suite/components/misc/test_stringops.cpp @@ -12,68 +12,3 @@ struct StringOpsTest : public ::testing::Test { } }; - -TEST_F(StringOpsTest, begins_matching) -{ - ASSERT_TRUE(Misc::begins("abc", "a")); - ASSERT_TRUE(Misc::begins("abc", "ab")); - ASSERT_TRUE(Misc::begins("abc", "abc")); - ASSERT_TRUE(Misc::begins("abcd", "abc")); -} - -TEST_F(StringOpsTest, begins_not_matching) -{ - ASSERT_FALSE(Misc::begins("abc", "b")); - ASSERT_FALSE(Misc::begins("abc", "bc")); - ASSERT_FALSE(Misc::begins("abc", "bcd")); - ASSERT_FALSE(Misc::begins("abc", "abcd")); -} - -TEST_F(StringOpsTest, ibegins_matching) -{ - ASSERT_TRUE(Misc::ibegins("Abc", "a")); - ASSERT_TRUE(Misc::ibegins("aBc", "ab")); - ASSERT_TRUE(Misc::ibegins("abC", "abc")); - ASSERT_TRUE(Misc::ibegins("abcD", "abc")); -} - -TEST_F(StringOpsTest, ibegins_not_matching) -{ - ASSERT_FALSE(Misc::ibegins("abc", "b")); - ASSERT_FALSE(Misc::ibegins("abc", "bc")); - ASSERT_FALSE(Misc::ibegins("abc", "bcd")); - ASSERT_FALSE(Misc::ibegins("abc", "abcd")); -} - -TEST_F(StringOpsTest, ends_matching) -{ - ASSERT_TRUE(Misc::ends("abc", "c")); - ASSERT_TRUE(Misc::ends("abc", "bc")); - ASSERT_TRUE(Misc::ends("abc", "abc")); - ASSERT_TRUE(Misc::ends("abcd", "abcd")); -} - -TEST_F(StringOpsTest, ends_not_matching) -{ - ASSERT_FALSE(Misc::ends("abc", "b")); - ASSERT_FALSE(Misc::ends("abc", "ab")); - ASSERT_FALSE(Misc::ends("abc", "bcd")); - ASSERT_FALSE(Misc::ends("abc", "abcd")); -} - -TEST_F(StringOpsTest, iends_matching) -{ - ASSERT_TRUE(Misc::iends("Abc", "c")); - ASSERT_TRUE(Misc::iends("aBc", "bc")); - ASSERT_TRUE(Misc::iends("abC", "abc")); - ASSERT_TRUE(Misc::iends("abcD", "abcd")); -} - -TEST_F(StringOpsTest, iends_not_matching) -{ - ASSERT_FALSE(Misc::iends("abc", "b")); - ASSERT_FALSE(Misc::iends("abc", "ab")); - ASSERT_FALSE(Misc::iends("abc", "bcd")); - ASSERT_FALSE(Misc::iends("abc", "abcd")); -} - diff --git a/cmake/FindDirectX.cmake b/cmake/FindDirectX.cmake new file mode 100644 index 000000000..4641b55a3 --- /dev/null +++ b/cmake/FindDirectX.cmake @@ -0,0 +1,72 @@ +#------------------------------------------------------------------- +# This file is part of the CMake build system for OGRE +# (Object-oriented Graphics Rendering Engine) +# For the latest info, see http://www.ogre3d.org/ +# +# The contents of this file are placed in the public domain. Feel +# free to make use of it in any way you like. +#------------------------------------------------------------------- + +# ----------------------------------------------------------------------------- +# Find DirectX9 SDK +# Define: +# DirectX9_FOUND +# DirectX9_INCLUDE_DIR +# DirectX9_LIBRARY +# DirectX9_ROOT_DIR + +if(WIN32) # The only platform it makes sense to check for DirectX9 SDK + include(FindPkgMacros) + findpkg_begin(DirectX9) + + # Get path, convert backslashes as ${ENV_DXSDK_DIR} + getenv_path(DXSDK_DIR) + getenv_path(DirectX_HOME) + getenv_path(DirectX_ROOT) + getenv_path(DirectX_BASE) + + # construct search paths + set(DirectX9_PREFIX_PATH + "${DXSDK_DIR}" "${ENV_DXSDK_DIR}" + "${DIRECTX_HOME}" "${ENV_DIRECTX_HOME}" + "${DIRECTX_ROOT}" "${ENV_DIRECTX_ROOT}" + "${DIRECTX_BASE}" "${ENV_DIRECTX_BASE}" + "C:/apps_x86/Microsoft DirectX SDK*" + "C:/Program Files (x86)/Microsoft DirectX SDK*" + "C:/apps/Microsoft DirectX SDK*" + "C:/Program Files/Microsoft DirectX SDK*" + "$ENV{ProgramFiles}/Microsoft DirectX SDK*" + ) + + create_search_paths(DirectX9) + # redo search if prefix path changed + clear_if_changed(DirectX9_PREFIX_PATH + DirectX9_LIBRARY + DirectX9_INCLUDE_DIR + ) + + find_path(DirectX9_INCLUDE_DIR NAMES d3d9.h D3DCommon.h HINTS ${DirectX9_INC_SEARCH_PATH}) + # dlls are in DirectX9_ROOT_DIR/Developer Runtime/x64|x86 + # lib files are in DirectX9_ROOT_DIR/Lib/x64|x86 + if(CMAKE_CL_64) + set(DirectX9_LIBPATH_SUFFIX "x64") + else(CMAKE_CL_64) + set(DirectX9_LIBPATH_SUFFIX "x86") + endif(CMAKE_CL_64) + find_library(DirectX9_LIBRARY NAMES d3d9 HINTS ${DirectX9_LIB_SEARCH_PATH} PATH_SUFFIXES ${DirectX9_LIBPATH_SUFFIX}) + find_library(DirectX9_D3DX9_LIBRARY NAMES d3dx9 HINTS ${DirectX9_LIB_SEARCH_PATH} PATH_SUFFIXES ${DirectX9_LIBPATH_SUFFIX}) + find_library(DirectX9_DXERR_LIBRARY NAMES DxErr HINTS ${DirectX9_LIB_SEARCH_PATH} PATH_SUFFIXES ${DirectX9_LIBPATH_SUFFIX}) + find_library(DirectX9_DXGUID_LIBRARY NAMES dxguid HINTS ${DirectX9_LIB_SEARCH_PATH} PATH_SUFFIXES ${DirectX9_LIBPATH_SUFFIX}) + + findpkg_finish(DirectX9) + set(DirectX9_LIBRARIES ${DirectX9_LIBRARIES} + ${DirectX9_D3DX9_LIBRARY} + ${DirectX9_DXERR_LIBRARY} + ${DirectX9_DXGUID_LIBRARY} + ) + + mark_as_advanced(DirectX9_D3DX9_LIBRARY DirectX9_DXERR_LIBRARY DirectX9_DXGUID_LIBRARY + DirectX9_DXGI_LIBRARY DirectX9_D3DCOMPILER_LIBRARY) + + +endif(WIN32) diff --git a/cmake/FindDirectX11.cmake b/cmake/FindDirectX11.cmake new file mode 100644 index 000000000..22d5b5441 --- /dev/null +++ b/cmake/FindDirectX11.cmake @@ -0,0 +1,114 @@ +#------------------------------------------------------------------- +# This file is part of the CMake build system for OGRE +# (Object-oriented Graphics Rendering Engine) +# For the latest info, see http://www.ogre3d.org/ +# +# The contents of this file are placed in the public domain. Feel +# free to make use of it in any way you like. +#------------------------------------------------------------------- + +# ----------------------------------------------------------------------------- +# Find DirectX11 SDK +# Define: +# DirectX11_FOUND +# DirectX11_INCLUDE_DIR +# DirectX11_LIBRARY +# DirectX11_ROOT_DIR + +if(WIN32) # The only platform it makes sense to check for DirectX11 SDK + include(FindPkgMacros) + findpkg_begin(DirectX11) + + # Get path, convert backslashes as ${ENV_DXSDK_DIR} + getenv_path(DXSDK_DIR) + getenv_path(DIRECTX_HOME) + getenv_path(DIRECTX_ROOT) + getenv_path(DIRECTX_BASE) + + # construct search paths + set(DirectX11_PREFIX_PATH + "${DXSDK_DIR}" "${ENV_DXSDK_DIR}" + "${DIRECTX_HOME}" "${ENV_DIRECTX_HOME}" + "${DIRECTX_ROOT}" "${ENV_DIRECTX_ROOT}" + "${DIRECTX_BASE}" "${ENV_DIRECTX_BASE}" + "C:/apps_x86/Microsoft DirectX SDK*" + "C:/Program Files (x86)/Microsoft DirectX SDK*" + "C:/apps/Microsoft DirectX SDK*" + "C:/Program Files/Microsoft DirectX SDK*" + "$ENV{ProgramFiles}/Microsoft DirectX SDK*" + ) + + if(OGRE_BUILD_PLATFORM_WINRT) + # Windows 8 SDK has custom layout + set(DirectX11_INC_SEARCH_PATH + "C:/Program Files (x86)/Windows Kits/8.0/Include/shared" + "C:/Program Files (x86)/Windows Kits/8.0/Include/um" + ) + set(DirectX11_LIB_SEARCH_PATH + "C:/Program Files (x86)/Windows Kits/8.0/Lib/win8/um" + ) + endif() + + create_search_paths(DirectX11) + # redo search if prefix path changed + clear_if_changed(DirectX11_PREFIX_PATH + DirectX11_LIBRARY + DirectX11_INCLUDE_DIR + ) + + # dlls are in DirectX11_ROOT_DIR/Developer Runtime/x64|x86 + # lib files are in DirectX11_ROOT_DIR/Lib/x64|x86 + if(CMAKE_CL_64) + set(DirectX11_LIBPATH_SUFFIX "x64") + else(CMAKE_CL_64) + set(DirectX11_LIBPATH_SUFFIX "x86") + endif(CMAKE_CL_64) + + # look for D3D11 components + find_path(DirectX11_INCLUDE_DIR NAMES D3D11Shader.h HINTS ${DirectX11_INC_SEARCH_PATH}) + find_library(DirectX11_DXERR_LIBRARY NAMES DxErr HINTS ${DirectX11_LIB_SEARCH_PATH} PATH_SUFFIXES ${DirectX11_LIBPATH_SUFFIX}) + find_library(DirectX11_DXGUID_LIBRARY NAMES dxguid HINTS ${DirectX11_LIB_SEARCH_PATH} PATH_SUFFIXES ${DirectX11_LIBPATH_SUFFIX}) + find_library(DirectX11_DXGI_LIBRARY NAMES dxgi HINTS ${DirectX11_LIB_SEARCH_PATH} PATH_SUFFIXES ${DirectX11_LIBPATH_SUFFIX}) + find_library(DirectX11_D3DCOMPILER_LIBRARY NAMES d3dcompiler HINTS ${DirectX11_LIB_SEARCH_PATH} PATH_SUFFIXES ${DirectX11_LIBPATH_SUFFIX}) + + find_library(DirectX11_LIBRARY NAMES d3d11 HINTS ${DirectX11_LIB_SEARCH_PATH} PATH_SUFFIXES ${DirectX11_LIBPATH_SUFFIX}) + find_library(DirectX11_D3DX11_LIBRARY NAMES d3dx11 HINTS ${DirectX11_LIB_SEARCH_PATH} PATH_SUFFIXES ${DirectX11_LIBPATH_SUFFIX}) + if (DirectX11_INCLUDE_DIR AND DirectX11_LIBRARY) + set(DirectX11_D3D11_FOUND TRUE) + set(DirectX11_INCLUDE_DIR ${DirectX11_INCLUDE_DIR}) + set(DirectX11_D3D11_LIBRARIES ${DirectX11_D3D11_LIBRARIES} + ${DirectX11_LIBRARY} + ${DirectX11_DXGI_LIBRARY} + ${DirectX11_DXGUID_LIBRARY} + ${DirectX11_D3DCOMPILER_LIBRARY} + ) + endif () + if (DirectX11_D3DX11_LIBRARY) + set(DirectX11_D3D11_LIBRARIES ${DirectX11_D3D11_LIBRARIES} ${DirectX11_D3DX11_LIBRARY}) + endif () + if (DirectX11_DXERR_LIBRARY) + set(DirectX11_D3D11_LIBRARIES ${DirectX11_D3D11_LIBRARIES} ${DirectX11_DXERR_LIBRARY}) + endif () + + findpkg_finish(DirectX11) + + set(DirectX11_LIBRARIES + ${DirectX11_D3D11_LIBRARIES} + ) + + if (OGRE_BUILD_PLATFORM_WINDOWS_PHONE) + set(DirectX11_FOUND TRUE) + set(DirectX11_INCLUDE_DIR "C:/Program Files (x86)/Microsoft Visual Studio 11.0/VC/WPSDK/WP80/include" CACHE STRING "" FORCE) + set(DirectX11_LIBRARY "C:/Program Files (x86)/Microsoft Visual Studio 11.0/VC/WPSDK/WP80/lib" CACHE STRING "" FORCE) + set(DirectX11_LIBRARIES ${DirectX11_LIBRARY}) + set(CMAKE_CXX_FLAGS "/EHsc" CACHE STRING "" FORCE) + endif () + + mark_as_advanced(DirectX11_INCLUDE_DIR + DirectX11_D3D11_LIBRARIES + DirectX11_D3DX11_LIBRARY + DirectX11_DXERR_LIBRARY + DirectX11_DXGUID_LIBRARY + DirectX11_DXGI_LIBRARY + DirectX11_D3DCOMPILER_LIBRARY) +endif(WIN32) \ No newline at end of file diff --git a/cmake/FindFFmpeg.cmake b/cmake/FindFFmpeg.cmake index a3509597b..74584bf31 100644 --- a/cmake/FindFFmpeg.cmake +++ b/cmake/FindFFmpeg.cmake @@ -14,6 +14,7 @@ # - AVUTIL # - POSTPROCESS # - SWSCALE +# - SWRESAMPLE # the following variables will be defined # _FOUND - System has # _INCLUDE_DIRS - Include directory necessary for using the headers @@ -112,6 +113,8 @@ if (NOT FFMPEG_LIBRARIES) find_component(AVUTIL libavutil avutil libavutil/avutil.h) find_component(SWSCALE libswscale swscale libswscale/swscale.h) find_component(POSTPROC libpostproc postproc libpostproc/postprocess.h) + find_component(SWRESAMPLE libswresample swresample libswresample/swresample.h) + find_component(AVRESAMPLE libavresample avresample libavresample/avresample.h) # Check if the required components were found and add their stuff to the FFMPEG_* vars. foreach (_component ${FFmpeg_FIND_COMPONENTS}) @@ -142,7 +145,7 @@ if (NOT FFMPEG_LIBRARIES) endif () # Now set the noncached _FOUND vars for the components. -foreach (_component AVCODEC AVDEVICE AVFORMAT AVUTIL POSTPROCESS SWSCALE) +foreach (_component AVCODEC AVDEVICE AVFORMAT AVUTIL POSTPROCESS SWSCALE SWRESAMPLE AVRESAMPLE) set_component_found(${_component}) endforeach () diff --git a/cmake/FindMyGUI.cmake b/cmake/FindMyGUI.cmake index e2fefac7d..40fa2373f 100644 --- a/cmake/FindMyGUI.cmake +++ b/cmake/FindMyGUI.cmake @@ -12,6 +12,7 @@ # For details see the accompanying COPYING-CMAKE-SCRIPTS file. CMAKE_POLICY(PUSH) include(FindPkgMacros) +include(PreprocessorUtils) # IF (MYGUI_LIBRARIES AND MYGUI_INCLUDE_DIRS) # SET(MYGUI_FIND_QUIETLY TRUE) @@ -19,58 +20,34 @@ include(FindPkgMacros) IF (WIN32) #Windows MESSAGE(STATUS "Looking for MyGUI") -SET(MYGUISDK $ENV{MYGUI_HOME}) + SET(MYGUISDK $ENV{MYGUI_HOME}) IF (MYGUISDK) -findpkg_begin ( "MYGUI" ) + findpkg_begin ( "MYGUI" ) MESSAGE(STATUS "Using MyGUI in MyGUI SDK") -STRING(REGEX REPLACE "[\\]" "/" MYGUISDK "${MYGUISDK}" ) - -find_path ( MYGUI_INCLUDE_DIRS -MyGUI.h -"${MYGUISDK}/MyGUIEngine/include" -NO_DEFAULT_PATH ) - -find_path ( MYGUI_PLATFORM_INCLUDE_DIRS -MyGUI_OgrePlatform.h -"${MYGUISDK}/Platforms/Ogre/OgrePlatform/include" -NO_DEFAULT_PATH ) - -SET ( MYGUI_LIB_DIR ${MYGUISDK}/lib ${MYGUISDK}/*/lib ) - -find_library ( MYGUI_LIBRARIES_REL NAMES -MyGUIEngine.lib -MyGUI.OgrePlatform.lib -HINTS -${MYGUI_LIB_DIR} -PATH_SUFFIXES "" release relwithdebinfo minsizerel ) - -find_library ( MYGUI_LIBRARIES_DBG NAMES -MyGUIEngine_d.lib -MyGUI.OgrePlatform_d.lib -HINTS -${MYGUI_LIB_DIR} -PATH_SUFFIXES "" debug ) - -find_library ( MYGUI_PLATFORM_LIBRARIES_REL NAMES -MyGUI.OgrePlatform.lib -HINTS -${MYGUI_LIB_DIR} -PATH_SUFFIXES "" release relwithdebinfo minsizerel ) - -find_library ( MYGUI_PLATFORM_LIBRARIES_DBG NAMES -MyGUI.OgrePlatform_d.lib -HINTS -${MYGUI_LIB_DIR} -PATH_SUFFIXES "" debug ) - -make_library_set ( MYGUI_LIBRARIES ) -make_library_set ( MYGUI_PLATFORM_LIBRARIES ) - -MESSAGE ("${MYGUI_LIBRARIES}") -MESSAGE ("${MYGUI_PLATFORM_LIBRARIES}") - -#findpkg_finish ( "MYGUI" ) + STRING(REGEX REPLACE "[\\]" "/" MYGUISDK "${MYGUISDK}" ) + find_path ( MYGUI_INCLUDE_DIRS MyGUI.h "${MYGUISDK}/MyGUIEngine/include" NO_DEFAULT_PATH ) + find_path ( MYGUI_PLATFORM_INCLUDE_DIRS MyGUI_OgrePlatform.h "${MYGUISDK}/Platforms/Ogre/OgrePlatform/include" NO_DEFAULT_PATH ) + + SET ( MYGUI_LIB_DIR ${MYGUISDK}/lib ${MYGUISDK}/*/lib ) + + if ( MYGUI_STATIC ) + set(LIB_SUFFIX "Static") + endif ( MYGUI_STATIC ) + + find_library ( MYGUI_LIBRARIES_REL NAMES MyGUIEngine${LIB_SUFFIX}.lib MyGUI.OgrePlatform.lib HINTS ${MYGUI_LIB_DIR} PATH_SUFFIXES "" release relwithdebinfo minsizerel ) + find_library ( MYGUI_LIBRARIES_DBG NAMES MyGUIEngine${LIB_SUFFIX}_d.lib MyGUI.OgrePlatform_d.lib HINTS ${MYGUI_LIB_DIR} PATH_SUFFIXES "" debug ) + + find_library ( MYGUI_PLATFORM_LIBRARIES_REL NAMES MyGUI.OgrePlatform.lib HINTS ${MYGUI_LIB_DIR} PATH_SUFFIXES "" release relwithdebinfo minsizerel ) + find_library ( MYGUI_PLATFORM_LIBRARIES_DBG NAMES MyGUI.OgrePlatform_d.lib HINTS ${MYGUI_LIB_DIR} PATH_SUFFIXES "" debug ) + + make_library_set ( MYGUI_LIBRARIES ) + make_library_set ( MYGUI_PLATFORM_LIBRARIES ) + + MESSAGE ("${MYGUI_LIBRARIES}") + MESSAGE ("${MYGUI_PLATFORM_LIBRARIES}") + + #findpkg_finish ( "MYGUI" ) ENDIF (MYGUISDK) IF (OGRESOURCE) MESSAGE(STATUS "Using MyGUI in OGRE dependencies") @@ -155,6 +132,18 @@ IF (MYGUI_FOUND) MESSAGE(STATUS " libraries : ${MYGUI_LIBRARIES} from ${MYGUI_LIB_DIR}") MESSAGE(STATUS " includes : ${MYGUI_INCLUDE_DIRS}") ENDIF (NOT MYGUI_FIND_QUIETLY) + + find_file(MYGUI_PREQUEST_FILE NAMES MyGUI_Prerequest.h PATHS ${MYGUI_INCLUDE_DIRS}) + file(READ ${MYGUI_PREQUEST_FILE} MYGUI_TEMP_VERSION_CONTENT) + get_preprocessor_entry(MYGUI_TEMP_VERSION_CONTENT MYGUI_VERSION_MAJOR MYGUI_VERSION_MAJOR) + get_preprocessor_entry(MYGUI_TEMP_VERSION_CONTENT MYGUI_VERSION_MINOR MYGUI_VERSION_MINOR) + get_preprocessor_entry(MYGUI_TEMP_VERSION_CONTENT MYGUI_VERSION_PATCH MYGUI_VERSION_PATCH) + set(MYGUI_VERSION "${MYGUI_VERSION_MAJOR}.${MYGUI_VERSION_MINOR}.${MYGUI_VERSION_PATCH}") + + IF (NOT MYGUI_FIND_QUIETLY) + MESSAGE(STATUS "MyGUI version: ${MYGUI_VERSION}") + ENDIF (NOT MYGUI_FIND_QUIETLY) + ELSE (MYGUI_FOUND) IF (MYGUI_FIND_REQUIRED) MESSAGE(FATAL_ERROR "Could not find MYGUI") diff --git a/cmake/FindOGRE.cmake b/cmake/FindOGRE.cmake index 96f93cf34..f2acf9d33 100644 --- a/cmake/FindOGRE.cmake +++ b/cmake/FindOGRE.cmake @@ -18,7 +18,7 @@ # Once done, this will define # # OGRE_FOUND - system has OGRE -# OGRE_INCLUDE_DIRS - the OGRE include directories +# OGRE_INCLUDE_DIRS - the OGRE include directories # OGRE_LIBRARIES - link these to use the OGRE core # OGRE_BINARY_REL - location of the main Ogre binary (win32 non-static only, release) # OGRE_BINARY_DBG - location of the main Ogre binaries (win32 non-static only, debug) @@ -35,7 +35,7 @@ # # OGRE_${COMPONENT}_FOUND - ${COMPONENT} is available # OGRE_${COMPONENT}_INCLUDE_DIRS - additional include directories for ${COMPONENT} -# OGRE_${COMPONENT}_LIBRARIES - link these to use ${COMPONENT} +# OGRE_${COMPONENT}_LIBRARIES - link these to use ${COMPONENT} # OGRE_${COMPONENT}_BINARY_REL - location of the component binary (win32 non-static only, release) # OGRE_${COMPONENT}_BINARY_DBG - location of the component binary (win32 non-static only, debug) # @@ -110,9 +110,9 @@ if (OGRE_PREFIX_SOURCE AND OGRE_PREFIX_BUILD) set(OGRE_INC_SEARCH_PATH ${dir}/include ${OGRE_INC_SEARCH_PATH}) set(OGRE_LIB_SEARCH_PATH ${dir}/lib ${OGRE_LIB_SEARCH_PATH}) set(OGRE_BIN_SEARCH_PATH ${dir}/bin ${OGRE_BIN_SEARCH_PATH}) - set(OGRE_BIN_SEARCH_PATH ${dir}/Samples/Common/bin ${OGRE_BIN_SEARCH_PATH}) + set(OGRE_BIN_SEARCH_PATH ${dir}/Samples/Common/bin ${OGRE_BIN_SEARCH_PATH}) endforeach(dir) - + if (OGRE_PREFIX_DEPENDENCIES_DIR) set(OGRE_INC_SEARCH_PATH ${OGRE_PREFIX_DEPENDENCIES_DIR}/include ${OGRE_INC_SEARCH_PATH}) set(OGRE_LIB_SEARCH_PATH ${OGRE_PREFIX_DEPENDENCIES_DIR}/lib ${OGRE_LIB_SEARCH_PATH}) @@ -124,12 +124,12 @@ else() endif () # redo search if any of the environmental hints changed -set(OGRE_COMPONENTS Paging Terrain +set(OGRE_COMPONENTS Paging Terrain Overlay Plugin_BSPSceneManager Plugin_CgProgramManager Plugin_OctreeSceneManager Plugin_OctreeZone Plugin_PCZSceneManager Plugin_ParticleFX - RenderSystem_Direct3D10 RenderSystem_Direct3D9 RenderSystem_GL RenderSystem_GLES) -set(OGRE_RESET_VARS - OGRE_CONFIG_INCLUDE_DIR OGRE_INCLUDE_DIR + RenderSystem_Direct3D10 RenderSystem_Direct3D9 RenderSystem_GL RenderSystem_GLES2) +set(OGRE_RESET_VARS + OGRE_CONFIG_INCLUDE_DIR OGRE_INCLUDE_DIR OGRE_LIBRARY_FWK OGRE_LIBRARY_REL OGRE_LIBRARY_DBG OGRE_PLUGIN_DIR_DBG OGRE_PLUGIN_DIR_REL OGRE_MEDIA_DIR) foreach (comp ${OGRE_COMPONENTS}) @@ -148,7 +148,7 @@ if(NOT OGRE_BUILD_PLATFORM_IPHONE AND APPLE) # try to find framework on OSX findpkg_framework(OGRE) else() - set(OGRE_LIBRARY_FWK "") + set(OGRE_LIBRARY_FWK "") endif() # locate Ogre include files @@ -215,8 +215,8 @@ list(REMOVE_DUPLICATES OGRE_INCLUDE_DIR) findpkg_finish(OGRE) add_parent_dir(OGRE_INCLUDE_DIRS OGRE_INCLUDE_DIR) if (OGRE_SOURCE) - # If working from source rather than SDK, add samples include - set(OGRE_INCLUDE_DIRS ${OGRE_INCLUDE_DIRS} "${OGRE_SOURCE}/Samples/Common/include") + # If working from source rather than SDK, add samples include + set(OGRE_INCLUDE_DIRS ${OGRE_INCLUDE_DIRS} "${OGRE_SOURCE}/Samples/Common/include") endif() mark_as_advanced(OGRE_CONFIG_INCLUDE_DIR OGRE_MEDIA_DIR OGRE_PLUGIN_DIR_REL OGRE_PLUGIN_DIR_DBG) @@ -234,10 +234,10 @@ if (OGRE_STATIC) find_package(FreeImage QUIET) find_package(Freetype QUIET) find_package(OpenGL QUIET) - find_package(OpenGLES QUIET) + find_package(OpenGLES2 QUIET) find_package(ZLIB QUIET) find_package(ZZip QUIET) - if (UNIX AND NOT APPLE) + if (UNIX AND (NOT APPLE AND NOT ANDROID)) find_package(X11 QUIET) find_library(XAW_LIBRARY NAMES Xaw Xaw7 PATHS ${DEP_LIB_SEARCH_DIR} ${X11_LIB_SEARCH_PATH}) if (NOT XAW_LIBRARY OR NOT X11_Xt_FOUND) @@ -258,10 +258,16 @@ if (OGRE_STATIC) endif () endif () - set(OGRE_LIBRARIES ${OGRE_LIBRARIES} ${OGRE_LIBRARY_FWK} ${ZZip_LIBRARIES} ${ZLIB_LIBRARIES} - ${FreeImage_LIBRARIES} ${FREETYPE_LIBRARIES} +if (ANDROID) + set(OGRE_LIBRARIES ${OGRE_LIBRARIES} ${OGRE_LIBRARY_FWK} ${ZZip_LIBRARIES} ${ZLIB_LIBRARIES} + ${FreeImage_LIBRARIES} ${FREETYPE_LIBRARIES} + ${Cocoa_LIBRARIES} ${Carbon_LIBRARIES}) +else () + set(OGRE_LIBRARIES ${OGRE_LIBRARIES} ${OGRE_LIBRARY_FWK} ${ZZip_LIBRARIES} ${ZLIB_LIBRARIES} + ${FreeImage_LIBRARIES} ${FREETYPE_LIBRARIES} ${X11_LIBRARIES} ${X11_Xt_LIBRARIES} ${XAW_LIBRARY} ${X11_Xrandr_LIB} ${Cocoa_LIBRARIES} ${Carbon_LIBRARIES}) +endif() if (NOT ZLIB_FOUND OR NOT ZZip_FOUND) set(OGRE_DEPS_FOUND FALSE) @@ -272,10 +278,10 @@ if (OGRE_STATIC) if (NOT FREETYPE_FOUND) set(OGRE_DEPS_FOUND FALSE) endif () - if (UNIX AND NOT APPLE) - if (NOT X11_FOUND) + if (UNIX AND NOT APPLE AND NOT ANDROID) + if (NOT X11_FOUND) set(OGRE_DEPS_FOUND FALSE) - endif () + endif () endif () if (OGRE_CONFIG_THREADS) @@ -305,7 +311,7 @@ if (OGRE_STATIC) endif () endif () endif () - + if (NOT OGRE_DEPS_FOUND) pkg_message(OGRE "Could not find all required dependencies for the Ogre package.") set(OGRE_FOUND FALSE) @@ -323,13 +329,13 @@ set(OGRE_LIBRARY_DIRS ${OGRE_LIBRARY_DIR_REL} ${OGRE_LIBRARY_DIR_DBG}) # find binaries if (NOT OGRE_STATIC) - if (WIN32) - find_file(OGRE_BINARY_REL NAMES "OgreMain.dll" HINTS ${OGRE_BIN_SEARCH_PATH} + if (WIN32) + find_file(OGRE_BINARY_REL NAMES "OgreMain.dll" HINTS ${OGRE_BIN_SEARCH_PATH} PATH_SUFFIXES "" release relwithdebinfo minsizerel) - find_file(OGRE_BINARY_DBG NAMES "OgreMain_d.dll" HINTS ${OGRE_BIN_SEARCH_PATH} + find_file(OGRE_BINARY_DBG NAMES "OgreMain_d.dll" HINTS ${OGRE_BIN_SEARCH_PATH} PATH_SUFFIXES "" debug ) - endif() - mark_as_advanced(OGRE_BINARY_REL OGRE_BINARY_DBG) + endif() + mark_as_advanced(OGRE_BINARY_REL OGRE_BINARY_DBG) endif() @@ -337,7 +343,7 @@ endif() # Find Ogre components ######################################################### -set(OGRE_COMPONENT_SEARCH_PATH_REL +set(OGRE_COMPONENT_SEARCH_PATH_REL ${OGRE_LIBRARY_DIR_REL}/.. ${OGRE_LIBRARY_DIR_REL}/../.. ${OGRE_BIN_SEARCH_PATH} @@ -350,7 +356,7 @@ set(OGRE_COMPONENT_SEARCH_PATH_DBG macro(ogre_find_component COMPONENT HEADER) findpkg_begin(OGRE_${COMPONENT}) - find_path(OGRE_${COMPONENT}_INCLUDE_DIR NAMES ${HEADER} HINTS ${OGRE_INCLUDE_DIRS} ${OGRE_PREFIX_SOURCE} PATH_SUFFIXES ${COMPONENT} OGRE/${COMPONENT} Components/${COMPONENT}/include) + find_path(OGRE_${COMPONENT}_INCLUDE_DIR NAMES ${HEADER} HINTS ${OGRE_INCLUDE_DIRS} ${OGRE_INCLUDE_DIR}/OGRE/${COMPONENT} ${OGRE_PREFIX_SOURCE} PATH_SUFFIXES ${COMPONENT} OGRE/${COMPONENT} Components/${COMPONENT}/include) set(OGRE_${COMPONENT}_LIBRARY_NAMES "Ogre${COMPONENT}${OGRE_LIB_SUFFIX}") get_debug_names(OGRE_${COMPONENT}_LIBRARY_NAMES) find_library(OGRE_${COMPONENT}_LIBRARY_REL NAMES ${OGRE_${COMPONENT}_LIBRARY_NAMES} HINTS ${OGRE_LIBRARY_DIR_REL} PATH_SUFFIXES "" "release" "relwithdebinfo" "minsizerel") @@ -358,19 +364,24 @@ macro(ogre_find_component COMPONENT HEADER) make_library_set(OGRE_${COMPONENT}_LIBRARY) findpkg_finish(OGRE_${COMPONENT}) if (OGRE_${COMPONENT}_FOUND) + if (APPLE) + include_directories("${OGRE_INCLUDE_DIR}/OGRE/${COMPONENT}") + endif() # find binaries if (NOT OGRE_STATIC) - if (WIN32) - find_file(OGRE_${COMPONENT}_BINARY_REL NAMES "Ogre${COMPONENT}.dll" HINTS ${OGRE_COMPONENT_SEARCH_PATH_REL} PATH_SUFFIXES "" bin bin/release bin/relwithdebinfo bin/minsizerel release) - find_file(OGRE_${COMPONENT}_BINARY_DBG NAMES "Ogre${COMPONENT}_d.dll" HINTS ${OGRE_COMPONENT_SEARCH_PATH_DBG} PATH_SUFFIXES "" bin bin/debug debug) - endif() - mark_as_advanced(OGRE_${COMPONENT}_BINARY_REL OGRE_${COMPONENT}_BINARY_DBG) + if (WIN32) + find_file(OGRE_${COMPONENT}_BINARY_REL NAMES "Ogre${COMPONENT}.dll" HINTS ${OGRE_COMPONENT_SEARCH_PATH_REL} PATH_SUFFIXES "" bin bin/release bin/relwithdebinfo bin/minsizerel release) + find_file(OGRE_${COMPONENT}_BINARY_DBG NAMES "Ogre${COMPONENT}_d.dll" HINTS ${OGRE_COMPONENT_SEARCH_PATH_DBG} PATH_SUFFIXES "" bin bin/debug debug) + endif() + mark_as_advanced(OGRE_${COMPONENT}_BINARY_REL OGRE_${COMPONENT}_BINARY_DBG) endif() endif() endmacro() # look for Paging component ogre_find_component(Paging OgrePaging.h) +# look for Overlay component +ogre_find_component(Overlay OgreOverlaySystem.h) # look for Terrain component ogre_find_component(Terrain OgreTerrain.h) # look for Property component @@ -389,17 +400,17 @@ macro(ogre_find_plugin PLUGIN HEADER) set(TMP_CMAKE_LIB_PREFIX ${CMAKE_FIND_LIBRARY_PREFIXES}) set(CMAKE_FIND_LIBRARY_PREFIXES ${CMAKE_FIND_LIBRARY_PREFIXES} "") endif() - + # strip RenderSystem_ or Plugin_ prefix from plugin name string(REPLACE "RenderSystem_" "" PLUGIN_TEMP ${PLUGIN}) string(REPLACE "Plugin_" "" PLUGIN_NAME ${PLUGIN_TEMP}) - + # header files for plugins are not usually needed, but find them anyway if they are present set(OGRE_PLUGIN_PATH_SUFFIXES - PlugIns PlugIns/${PLUGIN_NAME} Plugins Plugins/${PLUGIN_NAME} ${PLUGIN} + PlugIns PlugIns/${PLUGIN_NAME} Plugins Plugins/${PLUGIN_NAME} ${PLUGIN} RenderSystems RenderSystems/${PLUGIN_NAME} ${ARGN}) - find_path(OGRE_${PLUGIN}_INCLUDE_DIR NAMES ${HEADER} - HINTS ${OGRE_INCLUDE_DIRS} ${OGRE_PREFIX_SOURCE} + find_path(OGRE_${PLUGIN}_INCLUDE_DIR NAMES ${HEADER} + HINTS ${OGRE_INCLUDE_DIRS} ${OGRE_PREFIX_SOURCE} PATH_SUFFIXES ${OGRE_PLUGIN_PATH_SUFFIXES}) # find link libraries for plugins set(OGRE_${PLUGIN}_LIBRARY_NAMES "${PLUGIN}${OGRE_LIB_SUFFIX}") @@ -437,15 +448,15 @@ macro(ogre_find_plugin PLUGIN HEADER) if (OGRE_${PLUGIN}_FOUND) if (NOT OGRE_PLUGIN_DIR_REL OR NOT OGRE_PLUGIN_DIR_DBG) if (WIN32) - set(OGRE_PLUGIN_SEARCH_PATH_REL + set(OGRE_PLUGIN_SEARCH_PATH_REL ${OGRE_LIBRARY_DIR_REL}/.. ${OGRE_LIBRARY_DIR_REL}/../.. - ${OGRE_BIN_SEARCH_PATH} + ${OGRE_BIN_SEARCH_PATH} ) set(OGRE_PLUGIN_SEARCH_PATH_DBG ${OGRE_LIBRARY_DIR_DBG}/.. ${OGRE_LIBRARY_DIR_DBG}/../.. - ${OGRE_BIN_SEARCH_PATH} + ${OGRE_BIN_SEARCH_PATH} ) find_path(OGRE_PLUGIN_DIR_REL NAMES "${PLUGIN}.dll" HINTS ${OGRE_PLUGIN_SEARCH_PATH_REL} PATH_SUFFIXES "" bin bin/release bin/relwithdebinfo bin/minsizerel release) @@ -462,16 +473,16 @@ macro(ogre_find_plugin PLUGIN HEADER) set(OGRE_PLUGIN_DIR_DBG ${OGRE_PLUGIN_DIR_TMP}) endif () endif () - - # find binaries - if (NOT OGRE_STATIC) - if (WIN32) - find_file(OGRE_${PLUGIN}_REL NAMES "${PLUGIN}.dll" HINTS ${OGRE_PLUGIN_DIR_REL}) - find_file(OGRE_${PLUGIN}_DBG NAMES "${PLUGIN}_d.dll" HINTS ${OGRE_PLUGIN_DIR_DBG}) - endif() - mark_as_advanced(OGRE_${PLUGIN}_REL OGRE_${PLUGIN}_DBG) - endif() - + + # find binaries + if (NOT OGRE_STATIC) + if (WIN32) + find_file(OGRE_${PLUGIN}_REL NAMES "${PLUGIN}.dll" HINTS ${OGRE_PLUGIN_DIR_REL}) + find_file(OGRE_${PLUGIN}_DBG NAMES "${PLUGIN}_d.dll" HINTS ${OGRE_PLUGIN_DIR_DBG}) + endif() + mark_as_advanced(OGRE_${PLUGIN}_REL OGRE_${PLUGIN}_DBG) + endif() + endif () if (TMP_CMAKE_LIB_PREFIX) @@ -486,21 +497,25 @@ ogre_find_plugin(Plugin_CgProgramManager OgreCgProgram.h PlugIns/CgProgramManage ogre_find_plugin(Plugin_OctreeSceneManager OgreOctreeSceneManager.h PlugIns/OctreeSceneManager/include) ogre_find_plugin(Plugin_ParticleFX OgreParticleFXPrerequisites.h PlugIns/ParticleFX/include) ogre_find_plugin(RenderSystem_GL OgreGLRenderSystem.h RenderSystems/GL/include) -ogre_find_plugin(RenderSystem_GLES OgreGLESRenderSystem.h RenderSystems/GLES/include) +ogre_find_plugin(RenderSystem_GLES2 OgreGLES2RenderSystem.h RenderSystems/GLES2/include) ogre_find_plugin(RenderSystem_Direct3D9 OgreD3D9RenderSystem.h RenderSystems/Direct3D9/include) ogre_find_plugin(RenderSystem_Direct3D10 OgreD3D10RenderSystem.h RenderSystems/Direct3D10/include) ogre_find_plugin(RenderSystem_Direct3D11 OgreD3D11RenderSystem.h RenderSystems/Direct3D11/include) if (OGRE_STATIC) # check if dependencies for plugins are met - if (NOT DirectX_FOUND) + if (NOT DirectX9_FOUND) set(OGRE_RenderSystem_Direct3D9_FOUND FALSE) + else () + set(OGRE_INCLUDE_DIRS ${OGRE_INCLUDE_DIRS} ${DirectX9_INCLUDE_DIR}) endif () if (NOT DirectX_D3D10_FOUND) set(OGRE_RenderSystem_Direct3D10_FOUND FALSE) endif () if (NOT DirectX_D3D11_FOUND) set(OGRE_RenderSystem_Direct3D11_FOUND FALSE) + else () + set(OGRE_INCLUDE_DIRS ${OGRE_INCLUDE_DIRS} ${DirectX_D3D11_INCLUDE_DIR}) endif () if (NOT OPENGL_FOUND) set(OGRE_RenderSystem_GL_FOUND FALSE) @@ -511,9 +526,9 @@ if (OGRE_STATIC) if (NOT Cg_FOUND) set(OGRE_Plugin_CgProgramManager_FOUND FALSE) endif () - + set(OGRE_RenderSystem_Direct3D9_LIBRARIES ${OGRE_RenderSystem_Direct3D9_LIBRARIES} - ${DirectX_LIBRARIES} + ${DirectX9_LIBRARIES} ) set(OGRE_RenderSystem_Direct3D10_LIBRARIES ${OGRE_RenderSystem_Direct3D10_LIBRARIES} ${DirectX_D3D10_LIBRARIES} @@ -524,8 +539,8 @@ if (OGRE_STATIC) set(OGRE_RenderSystem_GL_LIBRARIES ${OGRE_RenderSystem_GL_LIBRARIES} ${OPENGL_LIBRARIES} ) - set(OGRE_RenderSystem_GLES_LIBRARIES ${OGRE_RenderSystem_GLES_LIBRARIES} - ${OPENGLES_LIBRARIES} + set(OGRE_RenderSystem_GLES2_LIBRARIES ${OGRE_RenderSystem_GLES2_LIBRARIES} + ${OPENGLES2_LIBRARIES} ) set(OGRE_Plugin_CgProgramManager_LIBRARIES ${OGRE_Plugin_CgProgramManager_LIBRARIES} ${Cg_LIBRARIES} diff --git a/cmake/FindSDL2.cmake b/cmake/FindSDL2.cmake index 70e607a89..163abf463 100644 --- a/cmake/FindSDL2.cmake +++ b/cmake/FindSDL2.cmake @@ -87,7 +87,7 @@ FIND_PATH(SDL2_INCLUDE_DIR SDL.h ) #MESSAGE("SDL2_INCLUDE_DIR is ${SDL2_INCLUDE_DIR}") -FIND_LIBRARY(SDL2_LIBRARY_TEMP +FIND_LIBRARY(SDL2_LIBRARY_PATH NAMES SDL2 HINTS $ENV{SDL2DIR} @@ -99,6 +99,9 @@ FIND_LIBRARY(SDL2_LIBRARY_TEMP /opt ) +set(SDL2_LIBRARY_ONLY ${SDL2_LIBRARY_PATH} CACHE STRING "The SDL2 library, with no other libraries.") +set(SDL2_LIBRARY_TEMP ${SDL2_LIBRARY_PATH}) + #MESSAGE("SDL2_LIBRARY_TEMP is ${SDL2_LIBRARY_TEMP}") IF(NOT SDL2_BUILDING_LIBRARY) diff --git a/components/CMakeLists.txt b/components/CMakeLists.txt index fa92f38fe..6a1db371d 100644 --- a/components/CMakeLists.txt +++ b/components/CMakeLists.txt @@ -19,7 +19,11 @@ add_component_dir (bsa ) add_component_dir (nif - controlled effect niftypes record controller extra node record_ptr data niffile property + controlled effect niftypes record controller extra node record_ptr data niffile property nifkey data node base nifstream + ) + +add_component_dir (nifcache + nifcache ) add_component_dir (nifogre @@ -41,15 +45,24 @@ add_component_dir (esm loadnpc loadpgrd loadrace loadregn loadscpt loadskil loadsndg loadsoun loadspel loadsscr loadstat loadweap records aipackage effectlist spelllist variant variantimp loadtes3 cellref filter savedgame journalentry queststate locals globalscript player objectstate cellid cellstate globalmap lightstate inventorystate containerstate npcstate creaturestate dialoguestate statstate - npcstats creaturestats weatherstate quickkeys fogstate spellstate activespells creaturelevliststate doorstate projectilestate + npcstats creaturestats weatherstate quickkeys fogstate spellstate activespells creaturelevliststate doorstate projectilestate debugprofile + aisequence magiceffects + ) + +add_component_dir (esmterrain + storage ) add_component_dir (misc - utf8stream stringops + utf8stream stringops resourcehelpers ) +IF(NOT WIN32 AND NOT APPLE) + add_definitions(-DGLOBAL_DATA_PATH="${GLOBAL_DATA_PATH}") + add_definitions(-DGLOBAL_CONFIG_PATH="${GLOBAL_CONFIG_PATH}") +ENDIF() add_component_dir (files - linuxpath windowspath macospath fixedpath multidircollection collections configurationmanager + linuxpath androidpath windowspath macospath fixedpath multidircollection collections configurationmanager constrainedfiledatastream lowlevelfile ) @@ -57,7 +70,7 @@ add_component_dir (compiler context controlparser errorhandler exception exprparser extensions fileparser generator lineparser literals locals output parser scanner scriptparser skipparser streamerrorhandler stringparser tokenloc nullerrorhandler opcodes extensions0 declarationparser - quickfileparser + quickfileparser discardparser ) add_component_dir (interpreter @@ -71,7 +84,7 @@ add_component_dir (translation add_definitions(-DTERRAIN_USE_SHADER=1) add_component_dir (terrain - quadtreenode chunk world storage material buffercache defs + quadtreenode chunk world defaultworld terraingrid storage material buffercache defs ) add_component_dir (loadinglistener @@ -82,6 +95,14 @@ add_component_dir (ogreinit ogreinit ogreplugin ) +add_component_dir (widgets + box imagebutton tags list numericeditbox widgets + ) + +add_component_dir (fontloader + fontloader + ) + add_component_dir (version version ) @@ -112,6 +133,12 @@ if(QT_QTGUI_LIBRARY AND QT_QTCORE_LIBRARY) QT4_WRAP_CPP(MOC_SRCS ${COMPONENT_MOC_FILES}) endif(QT_QTGUI_LIBRARY AND QT_QTCORE_LIBRARY) +if (CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") + if("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "x86_64" AND NOT APPLE) + add_definitions(-fPIC) + endif() +endif () + include_directories(${BULLET_INCLUDE_DIRS} ${CMAKE_CURRENT_BINARY_DIR}) add_library(components STATIC ${COMPONENT_FILES} ${MOC_SRCS} ${ESM_UI_HDR}) diff --git a/components/bsa/bsa_archive.cpp b/components/bsa/bsa_archive.cpp index 6574f096b..4f656f9c4 100644 --- a/components/bsa/bsa_archive.cpp +++ b/components/bsa/bsa_archive.cpp @@ -45,7 +45,7 @@ static char strict_normalize_char(char ch) static char nonstrict_normalize_char(char ch) { - return ch == '\\' ? '/' : std::tolower(ch); + return ch == '\\' ? '/' : std::tolower(ch,std::locale::classic()); } template diff --git a/components/bsa/tests/.gitignore b/components/bsa/tests/.gitignore deleted file mode 100644 index e2f2f332d..000000000 --- a/components/bsa/tests/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -*_test -bsatool -*.bsa diff --git a/components/bsa/tests/Makefile b/components/bsa/tests/Makefile deleted file mode 100644 index 73e20d7b3..000000000 --- a/components/bsa/tests/Makefile +++ /dev/null @@ -1,15 +0,0 @@ -GCC=g++ - -all: bsa_file_test ogre_archive_test - -I_OGRE=$(shell pkg-config --cflags OGRE) -L_OGRE=$(shell pkg-config --libs OGRE) - -bsa_file_test: bsa_file_test.cpp ../bsa_file.cpp - $(GCC) $^ -o $@ - -ogre_archive_test: ogre_archive_test.cpp ../bsa_file.cpp ../bsa_archive.cpp - $(GCC) $^ -o $@ $(I_OGRE) $(L_OGRE) - -clean: - rm *_test diff --git a/components/bsa/tests/bsa_file_test.cpp b/components/bsa/tests/bsa_file_test.cpp deleted file mode 100644 index 07ee73d17..000000000 --- a/components/bsa/tests/bsa_file_test.cpp +++ /dev/null @@ -1,44 +0,0 @@ -#include "../bsa_file.hpp" - -/* - Test of the BSAFile class - - This test requires that data/Morrowind.bsa exists in the root - directory of OpenMW. - - */ - -#include - -using namespace std; -using namespace Bsa; - -BSAFile bsa; - -void find(const char* file) -{ - cout << "Does file '" << file << "' exist?\n "; - if(bsa.exists(file)) - { - cout << "Yes.\n "; - cout << bsa.getFile(file)->size() << " bytes\n"; - } - else cout << "No.\n"; -} - -int main() -{ - cout << "Reading Morrowind.bsa\n"; - bsa.open("../../data/Morrowind.bsa"); - - const BSAFile::FileList &files = bsa.getList(); - - cout << "First 10 files in archive:\n"; - for(int i=0; i<10; i++) - cout << " " << files[i].name - << " (" << files[i].fileSize << " bytes @" - << files[i].offset << ")\n"; - - find("meshes\\r\\xnetch_betty.nif"); - find("humdrum"); -} diff --git a/components/bsa/tests/ogre_archive_test.cpp b/components/bsa/tests/ogre_archive_test.cpp deleted file mode 100644 index 9cd57240f..000000000 --- a/components/bsa/tests/ogre_archive_test.cpp +++ /dev/null @@ -1,34 +0,0 @@ -#include -#include - -// This is a test of the BSA archive handler for OGRE. - -#include "../bsa_archive.hpp" - -using namespace Ogre; -using namespace std; - -int main() -{ - // Disable Ogre logging - new LogManager; - Log *log = LogManager::getSingleton().createLog(""); - log->setDebugOutputEnabled(false); - - // Set up Root - Root *root = new Root("","",""); - - // Add the BSA - Bsa::addBSA("../../data/Morrowind.bsa"); - - // Pick a sample file - String tex = "textures\\tx_natural_cavern_wall13.dds"; - cout << "Opening file: " << tex << endl; - - // Get it from the resource system - DataStreamPtr data = ResourceGroupManager::getSingleton().openResource(tex, "General"); - - cout << "Size: " << data->size() << endl; - - return 0; -} diff --git a/components/bsa/tests/output/bsa_file_test.out b/components/bsa/tests/output/bsa_file_test.out deleted file mode 100644 index 0d8e76cdd..000000000 --- a/components/bsa/tests/output/bsa_file_test.out +++ /dev/null @@ -1,17 +0,0 @@ -Reading Morrowind.bsa -First 10 files in archive: - meshes\m\probe_journeyman_01.nif (6276 bytes @126646052) - textures\menu_rightbuttonup_top.dds (256 bytes @218530052) - textures\menu_rightbuttonup_right.dds (256 bytes @218529796) - textures\menu_rightbuttonup_left.dds (256 bytes @218529540) - textures\menu_rightbuttondown_top.dds (256 bytes @218528196) - meshes\b\b_n_redguard_f_skins.nif (41766 bytes @17809778) - meshes\b\b_n_redguard_m_skins.nif (41950 bytes @18103107) - meshes\b\b_n_redguard_f_wrist.nif (2355 bytes @17858132) - meshes\b\b_n_redguard_m_foot.nif (4141 bytes @17862081) - meshes\b\b_n_redguard_m_knee.nif (2085 bytes @18098101) -Does file 'meshes\r\xnetch_betty.nif' exist? - Yes. - 53714 bytes -Does file 'humdrum' exist? - No. diff --git a/components/bsa/tests/output/ogre_archive_test.out b/components/bsa/tests/output/ogre_archive_test.out deleted file mode 100644 index 748e4b1e7..000000000 --- a/components/bsa/tests/output/ogre_archive_test.out +++ /dev/null @@ -1,2 +0,0 @@ -Opening file: textures\tx_natural_cavern_wall13.dds -Size: 43808 diff --git a/components/bsa/tests/test.sh b/components/bsa/tests/test.sh deleted file mode 100755 index 2d07708ad..000000000 --- a/components/bsa/tests/test.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/bash - -make || exit - -mkdir -p output - -PROGS=*_test - -for a in $PROGS; do - if [ -f "output/$a.out" ]; then - echo "Running $a:" - ./$a | diff output/$a.out - - else - echo "Creating $a.out" - ./$a > "output/$a.out" - git add "output/$a.out" - fi -done diff --git a/components/compiler/controlparser.hpp b/components/compiler/controlparser.hpp index 1175a0ed5..59958fd90 100644 --- a/components/compiler/controlparser.hpp +++ b/components/compiler/controlparser.hpp @@ -52,7 +52,7 @@ namespace Compiler Literals& literals); void appendCode (std::vector& code) const; - ///< store generated code in \Ʀ code. + ///< store generated code in \a code. virtual bool parseName (const std::string& name, const TokenLoc& loc, Scanner& scanner); diff --git a/components/compiler/discardparser.cpp b/components/compiler/discardparser.cpp new file mode 100644 index 000000000..6028968bb --- /dev/null +++ b/components/compiler/discardparser.cpp @@ -0,0 +1,70 @@ + +#include "discardparser.hpp" + +#include "scanner.hpp" + +namespace Compiler +{ + DiscardParser::DiscardParser (ErrorHandler& errorHandler, const Context& context) + : Parser (errorHandler, context), mState (StartState) + { + + } + + bool DiscardParser::parseInt (int value, const TokenLoc& loc, Scanner& scanner) + { + if (mState==StartState || mState==CommaState || mState==MinusState) + { + start(); + return false; + } + + return Parser::parseInt (value, loc, scanner); + } + + bool DiscardParser::parseFloat (float value, const TokenLoc& loc, Scanner& scanner) + { + if (mState==StartState || mState==CommaState || mState==MinusState) + { + start(); + return false; + } + + return Parser::parseFloat (value, loc, scanner); + } + + bool DiscardParser::parseName (const std::string& name, const TokenLoc& loc, + Scanner& scanner) + { + if (mState==StartState || mState==CommaState) + { + start(); + return false; + } + + return Parser::parseName (name, loc, scanner); + } + + bool DiscardParser::parseSpecial (int code, const TokenLoc& loc, Scanner& scanner) + { + if (code==Scanner::S_comma && mState==StartState) + { + mState = CommaState; + return true; + } + + if (code==Scanner::S_minus && (mState==StartState || mState==CommaState)) + { + mState = MinusState; + return true; + } + + return Parser::parseSpecial (code, loc, scanner); + } + + void DiscardParser::reset() + { + mState = StartState; + Parser::reset(); + } +} diff --git a/components/compiler/discardparser.hpp b/components/compiler/discardparser.hpp new file mode 100644 index 000000000..bee8a87bb --- /dev/null +++ b/components/compiler/discardparser.hpp @@ -0,0 +1,45 @@ +#ifndef COMPILER_DISCARDPARSER_H_INCLUDED +#define COMPILER_DISCARDPARSER_H_INCLUDED + +#include "parser.hpp" + +namespace Compiler +{ + /// \brief Parse a single optional numeric value or string and discard it + class DiscardParser : public Parser + { + enum State + { + StartState, CommaState, MinusState + }; + + State mState; + + public: + + DiscardParser (ErrorHandler& errorHandler, const Context& context); + + virtual bool parseInt (int value, const TokenLoc& loc, Scanner& scanner); + ///< Handle an int token. + /// \return fetch another token? + + virtual bool parseFloat (float value, const TokenLoc& loc, Scanner& scanner); + ///< Handle a float token. + /// \return fetch another token? + + virtual bool parseName (const std::string& name, const TokenLoc& loc, + Scanner& scanner); + ///< Handle a name token. + /// \return fetch another token? + + virtual bool parseSpecial (int code, const TokenLoc& loc, Scanner& scanner); + ///< Handle a special character token. + /// \return fetch another token? + + virtual void reset(); + ///< Reset parser to clean state. + }; +} + +#endif + diff --git a/components/compiler/errorhandler.cpp b/components/compiler/errorhandler.cpp index fe58836cc..bcd30ef2d 100644 --- a/components/compiler/errorhandler.cpp +++ b/components/compiler/errorhandler.cpp @@ -3,11 +3,8 @@ namespace Compiler { - // constructor - - ErrorHandler::ErrorHandler() : mWarnings (0), mErrors (0), mWarningsMode (1) {} - - // destructor + ErrorHandler::ErrorHandler() + : mWarnings (0), mErrors (0), mWarningsMode (1), mDowngradeErrors (false) {} ErrorHandler::~ErrorHandler() {} @@ -49,6 +46,12 @@ namespace Compiler void ErrorHandler::error (const std::string& message, const TokenLoc& loc) { + if (mDowngradeErrors) + { + warning (message, loc); + return; + } + ++mErrors; report (message, loc, ErrorMessage); } @@ -72,4 +75,21 @@ namespace Compiler { mWarningsMode = mode; } + + void ErrorHandler::downgradeErrors (bool downgrade) + { + mDowngradeErrors = downgrade; + } + + + ErrorDowngrade::ErrorDowngrade (ErrorHandler& handler) : mHandler (handler) + { + mHandler.downgradeErrors (true); + } + + ErrorDowngrade::~ErrorDowngrade() + { + mHandler.downgradeErrors (false); + } + } diff --git a/components/compiler/errorhandler.hpp b/components/compiler/errorhandler.hpp index e5922a6be..c92e7bb8d 100644 --- a/components/compiler/errorhandler.hpp +++ b/components/compiler/errorhandler.hpp @@ -17,6 +17,7 @@ namespace Compiler int mWarnings; int mErrors; int mWarningsMode; + bool mDowngradeErrors; protected: @@ -66,6 +67,26 @@ namespace Compiler void setWarningsMode (int mode); ///< // 0 ignore, 1 rate as warning, 2 rate as error + + /// Treat errors as warnings. + void downgradeErrors (bool downgrade); + }; + + class ErrorDowngrade + { + ErrorHandler& mHandler; + + /// not implemented + ErrorDowngrade (const ErrorDowngrade&); + + /// not implemented + ErrorDowngrade& operator= (const ErrorDowngrade&); + + public: + + ErrorDowngrade (ErrorHandler& handler); + + ~ErrorDowngrade(); }; } diff --git a/components/compiler/exprparser.cpp b/components/compiler/exprparser.cpp index ed628278b..6dcca08df 100644 --- a/components/compiler/exprparser.cpp +++ b/components/compiler/exprparser.cpp @@ -16,6 +16,7 @@ #include "stringparser.hpp" #include "extensions.hpp" #include "context.hpp" +#include "discardparser.hpp" namespace Compiler { @@ -386,6 +387,9 @@ namespace Compiler mExplicit.clear(); mRefOp = false; + std::vector ignore; + parseArguments ("x", scanner, ignore); + mNextOperand = false; return true; } @@ -404,6 +408,21 @@ namespace Compiler mNextOperand = false; return true; } + else if (keyword==Scanner::K_scriptrunning) + { + start(); + + mTokenLoc = loc; + parseArguments ("c", scanner); + + Generator::scriptRunning (mCode); + mOperands.push_back ('l'); + + mExplicit.clear(); + mRefOp = false; + mNextOperand = false; + return true; + } // check for custom extensions if (const Extensions *extensions = getContext().getExtensions()) @@ -527,6 +546,9 @@ namespace Compiler Generator::getDisabled (mCode, mLiterals, ""); mOperands.push_back ('l'); + std::vector ignore; + parseArguments ("x", scanner, ignore); + mNextOperand = false; return true; } @@ -737,6 +759,7 @@ namespace Compiler ExprParser parser (getErrorHandler(), getContext(), mLocals, mLiterals, true); StringParser stringParser (getErrorHandler(), getContext(), mLiterals); + DiscardParser discardParser (getErrorHandler(), getContext()); std::stack > stack; @@ -771,6 +794,27 @@ namespace Compiler ++optionalCount; } } + else if (*iter=='X') + { + parser.reset(); + + parser.setOptional (true); + + scanner.scan (parser); + + if (parser.isEmpty()) + break; + } + else if (*iter=='z') + { + discardParser.reset(); + discardParser.setOptional (true); + + scanner.scan (discardParser); + + if (discardParser.isEmpty()) + break; + } else { parser.reset(); diff --git a/components/compiler/extensions.hpp b/components/compiler/extensions.hpp index 3f91ca357..a15218d99 100644 --- a/components/compiler/extensions.hpp +++ b/components/compiler/extensions.hpp @@ -20,7 +20,9 @@ namespace Compiler l - Integer
s - Short
S - String, case preserved
- x - Optional, ignored argument + x - Optional, ignored string argument + X - Optional, ignored numeric expression + z - Optional, ignored string or numeric argument **/ typedef std::string ScriptArgs; @@ -105,7 +107,7 @@ namespace Compiler ///< Append code for function to \a code. void listKeywords (std::vector& keywords) const; - ///< Append all known keywords to \Ʀ kaywords. + ///< Append all known keywords to \a kaywords. }; } diff --git a/components/compiler/extensions0.cpp b/components/compiler/extensions0.cpp index 0f726a52d..8a17b5e79 100644 --- a/components/compiler/extensions0.cpp +++ b/components/compiler/extensions0.cpp @@ -70,6 +70,7 @@ namespace Compiler extensions.registerFunction ("getlineofsight", 'l', "c", opcodeGetLineOfSight, opcodeGetLineOfSightExplicit); extensions.registerFunction ("getlos", 'l', "c", opcodeGetLineOfSight, opcodeGetLineOfSightExplicit); extensions.registerFunction("gettarget", 'l', "c", opcodeGetTarget, opcodeGetTargetExplicit); + extensions.registerInstruction("face", "llX", opcodeFace, opcodeFaceExplicit); } } @@ -112,12 +113,12 @@ namespace Compiler { void registerExtensions (Extensions& extensions) { - extensions.registerInstruction ("additem", "cl", opcodeAddItem, opcodeAddItemExplicit); + extensions.registerInstruction ("additem", "clX", opcodeAddItem, opcodeAddItemExplicit); extensions.registerFunction ("getitemcount", 'l', "c", opcodeGetItemCount, opcodeGetItemCountExplicit); extensions.registerInstruction ("removeitem", "cl", opcodeRemoveItem, opcodeRemoveItemExplicit); - extensions.registerInstruction ("equip", "c", opcodeEquip, opcodeEquipExplicit); + extensions.registerInstruction ("equip", "cX", opcodeEquip, opcodeEquipExplicit); extensions.registerFunction ("getarmortype", 'l', "l", opcodeGetArmorType, opcodeGetArmorTypeExplicit); extensions.registerFunction ("hasitemequipped", 'l', "c", opcodeHasItemEquipped, opcodeHasItemEquippedExplicit); extensions.registerFunction ("hassoulgem", 'l', "c", opcodeHasSoulGem, opcodeHasSoulGemExplicit); @@ -147,6 +148,16 @@ namespace Compiler extensions.registerInstruction ("forcerun", "", opcodeForceRun, opcodeForceRunExplicit); + extensions.registerInstruction ("clearforcejump", "", opcodeClearForceJump, + opcodeClearForceJumpExplicit); + extensions.registerInstruction ("forcejump", "", opcodeForceJump, + opcodeForceJumpExplicit); + + extensions.registerInstruction ("clearforcemovejump", "", opcodeClearForceMoveJump, + opcodeClearForceMoveJumpExplicit); + extensions.registerInstruction ("forcemovejump", "", opcodeForceMoveJump, + opcodeForceMoveJumpExplicit); + extensions.registerInstruction ("clearforcesneak", "", opcodeClearForceSneak, opcodeClearForceSneakExplicit); extensions.registerInstruction ("forcesneak", "", opcodeForceSneak, @@ -154,6 +165,8 @@ namespace Compiler extensions.registerFunction ("getpcrunning", 'l', "", opcodeGetPcRunning); extensions.registerFunction ("getpcsneaking", 'l', "", opcodeGetPcSneaking); extensions.registerFunction ("getforcerun", 'l', "", opcodeGetForceRun, opcodeGetForceRunExplicit); + extensions.registerFunction ("getforcejump", 'l', "", opcodeGetForceJump, opcodeGetForceJumpExplicit); + extensions.registerFunction ("getforcemovejump", 'l', "", opcodeGetForceMoveJump, opcodeGetForceMoveJumpExplicit); extensions.registerFunction ("getforcesneak", 'l', "", opcodeGetForceSneak, opcodeGetForceSneakExplicit); } } @@ -167,7 +180,6 @@ namespace Compiler extensions.registerFunction ("getjournalindex", 'l', "c", opcodeGetJournalIndex); extensions.registerInstruction ("addtopic", "S" , opcodeAddTopic); extensions.registerInstruction ("choice", "/SlSlSlSlSlSlSlSlSlSlSlSlSlSlSlSl", opcodeChoice); - extensions.registerInstruction("forcegreeting","",opcodeForceGreeting); extensions.registerInstruction("forcegreeting","",opcodeForceGreeting, opcodeForceGreetingExplicit); extensions.registerInstruction("goodbye", "", opcodeGoodbye); @@ -180,7 +192,8 @@ namespace Compiler extensions.registerFunction("samefaction", 'l', "", opcodeSameFaction, opcodeSameFactionExplicit); extensions.registerInstruction("modfactionreaction", "ccl", opcodeModFactionReaction); - extensions.registerFunction("getfactionreaction", 'l', "ccl", opcodeGetFactionReaction); + extensions.registerFunction("getfactionreaction", 'l', "ccX", opcodeGetFactionReaction); + extensions.registerInstruction("clearinfoactor", "", opcodeClearInfoActor, opcodeClearInfoActorExplicit); } } @@ -213,8 +226,11 @@ namespace Compiler extensions.registerInstruction ("togglefullhelp", "", opcodeToggleFullHelp); extensions.registerInstruction ("tfh", "", opcodeToggleFullHelp); - extensions.registerInstruction ("showmap", "S", opcodeShowMap); + extensions.registerInstruction ("showmap", "Sxxxx", opcodeShowMap); extensions.registerInstruction ("fillmap", "", opcodeFillMap); + extensions.registerInstruction ("menutest", "/l", opcodeMenuTest); + extensions.registerInstruction ("togglemenus", "", opcodeToggleMenus); + extensions.registerInstruction ("tm", "", opcodeToggleMenus); } } @@ -240,6 +256,8 @@ namespace Compiler extensions.registerInstruction ("fadeto", "ff", opcodeFadeTo); extensions.registerInstruction ("togglewater", "", opcodeToggleWater); extensions.registerInstruction ("twa", "", opcodeToggleWater); + extensions.registerInstruction ("toggleworld", "", opcodeToggleWorld); + extensions.registerInstruction ("tw", "", opcodeToggleWorld); extensions.registerInstruction ("togglepathgrid", "", opcodeTogglePathgrid); extensions.registerInstruction ("tpg", "", opcodeTogglePathgrid); extensions.registerInstruction ("dontsaveobject", "", opcodeDontSaveObject); @@ -268,6 +286,10 @@ namespace Compiler extensions.registerInstruction ("fall", "", opcodeFall, opcodeFallExplicit); extensions.registerFunction ("getstandingpc", 'l', "", opcodeGetStandingPc, opcodeGetStandingPcExplicit); extensions.registerFunction ("getstandingactor", 'l', "", opcodeGetStandingActor, opcodeGetStandingActorExplicit); + extensions.registerFunction ("getcollidingpc", 'l', "", opcodeGetCollidingPc, opcodeGetCollidingPcExplicit); + extensions.registerFunction ("getcollidingactor", 'l', "", opcodeGetCollidingActor, opcodeGetCollidingActorExplicit); + extensions.registerInstruction ("hurtstandingactor", "f", opcodeHurtStandingActor, opcodeHurtStandingActorExplicit); + extensions.registerInstruction ("hurtcollidingactor", "f", opcodeHurtCollidingActor, opcodeHurtCollidingActorExplicit); extensions.registerFunction ("getwindspeed", 'f', "", opcodeGetWindSpeed); extensions.registerFunction ("hitonme", 'l', "S", opcodeHitOnMe, opcodeHitOnMeExplicit); extensions.registerInstruction ("disableteleporting", "", opcodeDisableTeleporting); @@ -280,6 +302,8 @@ namespace Compiler extensions.registerInstruction ("enablelevitation", "", opcodeEnableLevitation); extensions.registerFunction ("getpcinjail", 'l', "", opcodeGetPcInJail); extensions.registerFunction ("getpctraveling", 'l', "", opcodeGetPcTraveling); + extensions.registerInstruction ("betacomment", "S", opcodeBetaComment, opcodeBetaCommentExplicit); + extensions.registerInstruction ("bc", "S", opcodeBetaComment, opcodeBetaCommentExplicit); } } @@ -348,6 +372,16 @@ namespace Compiler "mercantile", "speechcraft", "handtohand" }; + static const char *magicEffects[numberOfMagicEffects] = + { + "resistmagicka", "resistfire", "resistfrost", "resistshock", + "resistdisease", "resistblight", "resistcorprus", "resistpoison", + "resistparalysis", "resistnormalweapons", "waterbreathing", "chameleon", + "waterwalking", "swimspeed", "superjump", "flying", + "armorbonus", "castpenalty", "silence", "blindness", + "paralysis", "invisible", "attackbonus", "defendbonus" + }; + std::string get ("get"); std::string set ("set"); std::string mod ("mod"); @@ -396,11 +430,23 @@ namespace Compiler opcodeModSkill+i, opcodeModSkillExplicit+i); } + for (int i=0; i& code) const; - ///< store generated code in \Ʀ code. + ///< store generated code in \a code. const Locals& getLocals() const; ///< get local variable declarations. diff --git a/components/compiler/generator.cpp b/components/compiler/generator.cpp index c67e51e57..2efa2477e 100644 --- a/components/compiler/generator.cpp +++ b/components/compiler/generator.cpp @@ -105,11 +105,6 @@ namespace code.push_back (Compiler::Generator::segment5 (17)); } - void opFloatToInt1 (Compiler::Generator::CodeContainer& code) - { - code.push_back (Compiler::Generator::segment5 (18)); - } - void opSquareRoot (Compiler::Generator::CodeContainer& code) { code.push_back (Compiler::Generator::segment5 (19)); @@ -300,9 +295,9 @@ namespace code.push_back (Compiler::Generator::segment5 (46)); } - void opStartScript (Compiler::Generator::CodeContainer& code) + void opStartScript (Compiler::Generator::CodeContainer& code, bool targeted) { - code.push_back (Compiler::Generator::segment5 (47)); + code.push_back (Compiler::Generator::segment5 (targeted ? 71 : 47)); } void opStopScript (Compiler::Generator::CodeContainer& code) @@ -606,16 +601,6 @@ namespace Compiler jump (code, offset); } - void jumpOnNonZero (CodeContainer& code, int offset) - { - opSkipOnZero (code); - - if (offset<0) - --offset; // compensate for skip instruction - - jump (code, offset); - } - void compare (CodeContainer& code, char op, char valueType1, char valueType2) { if (valueType1=='l' && valueType2=='l') @@ -830,9 +815,16 @@ namespace Compiler opScriptRunning (code); } - void startScript (CodeContainer& code) + void startScript (CodeContainer& code, Literals& literals, const std::string& id) { - opStartScript (code); + if (id.empty()) + opStartScript (code, false); + else + { + int index = literals.addString (id); + opPushInt (code, index); + opStartScript (code, true); + } } void stopScript (CodeContainer& code) diff --git a/components/compiler/generator.hpp b/components/compiler/generator.hpp index b51116122..a56d7c1f1 100644 --- a/components/compiler/generator.hpp +++ b/components/compiler/generator.hpp @@ -89,8 +89,6 @@ namespace Compiler void jumpOnZero (CodeContainer& code, int offset); - void jumpOnNonZero (CodeContainer& code, int offset); - void compare (CodeContainer& code, char op, char valueType1, char valueType2); void menuMode (CodeContainer& code); @@ -113,7 +111,7 @@ namespace Compiler void scriptRunning (CodeContainer& code); - void startScript (CodeContainer& code); + void startScript (CodeContainer& code, Literals& literals, const std::string& id); void stopScript (CodeContainer& code); diff --git a/components/compiler/lineparser.cpp b/components/compiler/lineparser.cpp index f7d2726e3..dc19b9a4b 100644 --- a/components/compiler/lineparser.cpp +++ b/components/compiler/lineparser.cpp @@ -1,6 +1,8 @@ #include "lineparser.hpp" +#include + #include #include "scanner.hpp" @@ -11,6 +13,7 @@ #include "generator.hpp" #include "extensions.hpp" #include "declarationparser.hpp" +#include "exception.hpp" namespace Compiler { @@ -54,7 +57,7 @@ namespace Compiler Literals& literals, std::vector& code, bool allowExpression) : Parser (errorHandler, context), mLocals (locals), mLiterals (literals), mCode (code), mState (BeginState), mExprParser (errorHandler, context, locals, literals), - mAllowExpression (allowExpression), mButtons(0), mType(0) + mAllowExpression (allowExpression), mButtons(0), mType(0), mReferenceMember(false) {} bool LineParser::parseInt (int value, const TokenLoc& loc, Scanner& scanner) @@ -119,7 +122,7 @@ namespace Compiler if (mState==SetMemberVarState) { - mMemberName = name; + mMemberName = Misc::StringUtils::lowerCase (name); std::pair type = getContext().getMemberType (mMemberName, mName); if (type.first!=' ') @@ -262,6 +265,20 @@ namespace Compiler Generator::disable (mCode, mLiterals, mExplicit); mState = PotentialEndState; return true; + + case Scanner::K_startscript: + + mExprParser.parseArguments ("c", scanner, mCode); + Generator::startScript (mCode, mLiterals, mExplicit); + mState = EndState; + return true; + + case Scanner::K_stopscript: + + mExprParser.parseArguments ("c", scanner, mCode); + Generator::stopScript (mCode); + mState = EndState; + return true; } // check for custom extensions @@ -278,9 +295,34 @@ namespace Compiler mExplicit.clear(); } - int optionals = mExprParser.parseArguments (argumentType, scanner, mCode); + try + { + // workaround for broken positioncell instructions. + /// \todo add option to disable this + std::auto_ptr errorDowngrade (0); + if (Misc::StringUtils::lowerCase (loc.mLiteral)=="positioncell") + errorDowngrade.reset (new ErrorDowngrade (getErrorHandler())); + + std::vector code; + int optionals = mExprParser.parseArguments (argumentType, scanner, code); + mCode.insert (mCode.end(), code.begin(), code.end()); + extensions->generateInstructionCode (keyword, mCode, mLiterals, + mExplicit, optionals); + } + catch (const SourceException&) + { + // Ignore argument exceptions for positioncell. + /// \todo add option to disable this + if (Misc::StringUtils::lowerCase (loc.mLiteral)=="positioncell") + { + SkipParser skip (getErrorHandler(), getContext()); + scanner.scan (skip); + return false; + } + + throw; + } - extensions->generateInstructionCode (keyword, mCode, mLiterals, mExplicit, optionals); mState = EndState; return true; } @@ -349,7 +391,7 @@ namespace Compiler if (declaration.parseKeyword (keyword, loc, scanner)) scanner.scan (declaration); - return true; + return false; } case Scanner::K_set: mState = SetState; return true; @@ -361,13 +403,6 @@ namespace Compiler mState = EndState; return true; - case Scanner::K_startscript: - - mExprParser.parseArguments ("c", scanner, mCode); - Generator::startScript (mCode); - mState = EndState; - return true; - case Scanner::K_stopscript: mExprParser.parseArguments ("c", scanner, mCode); diff --git a/components/compiler/locals.hpp b/components/compiler/locals.hpp index d5bf05253..1b2ae6042 100644 --- a/components/compiler/locals.hpp +++ b/components/compiler/locals.hpp @@ -15,10 +15,6 @@ namespace Compiler std::vector mLongs; std::vector mFloats; - int searchIndex (char type, const std::string& name) const; - - bool search (char type, const std::string& name) const; - std::vector& get (char type); public: @@ -29,6 +25,12 @@ namespace Compiler int getIndex (const std::string& name) const; ///< return index for local variable \a name (-1: does not exist). + bool search (char type, const std::string& name) const; + + /// Return index for local variable \a name of type \a type (-1: variable does not + /// exit). + int searchIndex (char type, const std::string& name) const; + const std::vector& get (char type) const; void write (std::ostream& localFile) const; diff --git a/components/compiler/opcodes.hpp b/components/compiler/opcodes.hpp index 8796c53c5..5063397e1 100644 --- a/components/compiler/opcodes.hpp +++ b/components/compiler/opcodes.hpp @@ -59,6 +59,8 @@ namespace Compiler const int opcodeStartCombatExplicit = 0x200023b; const int opcodeStopCombat = 0x200023c; const int opcodeStopCombatExplicit = 0x200023d; + const int opcodeFace = 0x200024c; + const int opcodeFaceExplicit = 0x200024d; } namespace Animation @@ -121,6 +123,14 @@ namespace Compiler const int opcodeClearForceRunExplicit = 0x2000155; const int opcodeForceRun = 0x2000156; const int opcodeForceRunExplicit = 0x2000157; + const int opcodeClearForceJump = 0x2000258; + const int opcodeClearForceJumpExplicit = 0x2000259; + const int opcodeForceJump = 0x200025a; + const int opcodeForceJumpExplicit = 0x200025b; + const int opcodeClearForceMoveJump = 0x200025c; + const int opcodeClearForceMoveJumpExplicit = 0x200025d; + const int opcodeForceMoveJump = 0x200025e; + const int opcodeForceMoveJumpExplicit = 0x200025f; const int opcodeClearForceSneak = 0x2000158; const int opcodeClearForceSneakExplicit = 0x2000159; const int opcodeForceSneak = 0x200015a; @@ -132,6 +142,10 @@ namespace Compiler const int opcodeGetForceSneak = 0x20001cc; const int opcodeGetForceRunExplicit = 0x20001cd; const int opcodeGetForceSneakExplicit = 0x20001ce; + const int opcodeGetForceJump = 0x2000260; + const int opcodeGetForceMoveJump = 0x2000262; + const int opcodeGetForceJumpExplicit = 0x2000261; + const int opcodeGetForceMoveJumpExplicit = 0x2000263; } namespace Dialogue @@ -154,6 +168,8 @@ namespace Compiler const int opcodeSameFactionExplicit = 0x20001b6; const int opcodeModFactionReaction = 0x2000242; const int opcodeGetFactionReaction = 0x2000243; + const int opcodeClearInfoActor = 0x2000245; + const int opcodeClearInfoActorExplicit = 0x2000246; } namespace Gui @@ -175,6 +191,8 @@ namespace Compiler const int opcodeToggleFullHelp = 0x2000151; const int opcodeShowMap = 0x20001a0; const int opcodeFillMap = 0x20001a1; + const int opcodeMenuTest = 0x2002c; + const int opcodeToggleMenus = 0x200024b; } namespace Misc @@ -194,6 +212,7 @@ namespace Compiler const int opcodeFadeOut = 0x200013d; const int opcodeFadeTo = 0x200013e; const int opcodeToggleWater = 0x2000144; + const int opcodeToggleWorld = 0x20002f5; const int opcodeTogglePathgrid = 0x2000146; const int opcodeDontSaveObject = 0x2000153; const int opcodeToggleVanityMode = 0x2000174; @@ -204,6 +223,8 @@ namespace Compiler const int opcodeGetLockedExplicit = 0x20001c8; const int opcodeGetEffect = 0x20001cf; const int opcodeGetEffectExplicit = 0x20001d0; + const int opcodeBetaComment = 0x2000247; + const int opcodeBetaCommentExplicit = 0x2000248; const int opcodeAddSoulGem = 0x20001f3; const int opcodeAddSoulGemExplicit = 0x20001f4; const int opcodeRemoveSoulGem = 0x20027; @@ -230,6 +251,14 @@ namespace Compiler const int opcodeGetStandingPcExplicit = 0x200020d; const int opcodeGetStandingActor = 0x200020e; const int opcodeGetStandingActorExplicit = 0x200020f; + const int opcodeGetCollidingPc = 0x2000250; + const int opcodeGetCollidingPcExplicit = 0x2000251; + const int opcodeGetCollidingActor = 0x2000252; + const int opcodeGetCollidingActorExplicit = 0x2000253; + const int opcodeHurtStandingActor = 0x2000254; + const int opcodeHurtStandingActorExplicit = 0x2000255; + const int opcodeHurtCollidingActor = 0x2000256; + const int opcodeHurtCollidingActorExplicit = 0x2000257; const int opcodeGetWindSpeed = 0x2000212; const int opcodePlayBink = 0x20001f7; const int opcodeGoToJail = 0x2000235; @@ -294,6 +323,8 @@ namespace Compiler const int numberOfDynamics = 3; const int numberOfSkills = 27; + const int numberOfMagicEffects = 24; + const int opcodeGetAttribute = 0x2000027; const int opcodeGetAttributeExplicit = 0x200002f; const int opcodeSetAttribute = 0x2000037; @@ -319,6 +350,13 @@ namespace Compiler const int opcodeModSkill = 0x20000fa; const int opcodeModSkillExplicit = 0x2000115; + const int opcodeGetMagicEffect = 0x2000264; + const int opcodeGetMagicEffectExplicit = 0x200027c; + const int opcodeSetMagicEffect = 0x2000294; + const int opcodeSetMagicEffectExplicit = 0x20002ac; + const int opcodeModMagicEffect = 0x20002c4; + const int opcodeModMagicEffectExplicit = 0x20002dc; + const int opcodeGetPCCrimeLevel = 0x20001ec; const int opcodeSetPCCrimeLevel = 0x20001ed; const int opcodeModPCCrimeLevel = 0x20001ee; @@ -380,6 +418,8 @@ namespace Compiler const int opcodeLowerRankExplicit = 0x20001eb; const int opcodeOnDeath = 0x20001fc; const int opcodeOnDeathExplicit = 0x2000205; + const int opcodeOnMurder = 0x2000249; + const int opcodeOnMurderExplicit = 0x200024a; const int opcodeOnKnockout = 0x2000240; const int opcodeOnKnockoutExplicit = 0x2000241; @@ -400,6 +440,9 @@ namespace Compiler const int opcodeRemoveEffectsExplicit = 0x200022e; const int opcodeResurrect = 0x200022f; const int opcodeResurrectExplicit = 0x2000230; + + const int opcodeGetStat = 0x200024e; + const int opcodeGetStatExplicit = 0x200024f; } namespace Transformation @@ -442,6 +485,7 @@ namespace Compiler const int opcodeMoveExplicit = 0x2000207; const int opcodeMoveWorld = 0x2000208; const int opcodeMoveWorldExplicit = 0x2000209; + const int opcodeResetActors = 0x20002f4; } namespace User diff --git a/components/compiler/output.hpp b/components/compiler/output.hpp index 37b88cee9..c544fb715 100644 --- a/components/compiler/output.hpp +++ b/components/compiler/output.hpp @@ -22,7 +22,7 @@ namespace Compiler Output (Locals& locals); void getCode (std::vector& code) const; - ///< store generated code in \Ʀ code. + ///< store generated code in \a code. const Literals& getLiterals() const; diff --git a/components/compiler/parser.cpp b/components/compiler/parser.cpp index 781fbad8c..0f442c350 100644 --- a/components/compiler/parser.cpp +++ b/components/compiler/parser.cpp @@ -21,13 +21,6 @@ namespace Compiler throw SourceException(); } - // Report the error - - void Parser::reportError (const std::string& message, const TokenLoc& loc) - { - mErrorHandler.error (message, loc); - } - // Report the warning without throwing an exception. void Parser::reportWarning (const std::string& message, const TokenLoc& loc) diff --git a/components/compiler/parser.hpp b/components/compiler/parser.hpp index 54e913b20..2ef6e85b9 100644 --- a/components/compiler/parser.hpp +++ b/components/compiler/parser.hpp @@ -26,9 +26,6 @@ namespace Compiler void reportSeriousError (const std::string& message, const TokenLoc& loc); ///< Report the error and throw a exception. - void reportError (const std::string& message, const TokenLoc& loc); - ///< Report the error - void reportWarning (const std::string& message, const TokenLoc& loc); ///< Report the warning without throwing an exception. diff --git a/components/compiler/scanner.cpp b/components/compiler/scanner.cpp index 46e50a2e9..16d54ff51 100644 --- a/components/compiler/scanner.cpp +++ b/components/compiler/scanner.cpp @@ -48,6 +48,9 @@ namespace Compiler bool Scanner::scanToken (Parser& parser) { + bool allowDigit = mNameStartingWithDigit; + mNameStartingWithDigit = false; + switch (mPutback) { case Putback_Special: @@ -112,6 +115,7 @@ namespace Compiler else if (isWhitespace (c)) { mLoc.mLiteral.clear(); + mNameStartingWithDigit = allowDigit; return true; } else if (c==':') @@ -120,21 +124,21 @@ namespace Compiler mLoc.mLiteral.clear(); return true; } - else if (std::isdigit (c)) + else if (std::isalpha (c) || c=='_' || c=='"' || (allowDigit && std::isdigit (c))) { bool cont = false; - if (scanInt (c, parser, cont)) + if (scanName (c, parser, cont)) { mLoc.mLiteral.clear(); return cont; } } - else if (std::isalpha (c) || c=='_' || c=='"') + else if (std::isdigit (c)) { bool cont = false; - if (scanName (c, parser, cont)) + if (scanInt (c, parser, cont)) { mLoc.mLiteral.clear(); return cont; @@ -343,17 +347,13 @@ namespace Compiler } else if (!(c=='"' && name.empty())) { - if (!(std::isalpha (c) || std::isdigit (c) || c=='_' || c=='`' || - /// \todo add an option to disable the following hack. Also, find out who is - /// responsible for allowing it in the first place and meet up with that person in - /// a dark alley. - (c=='-' && !name.empty() && std::isalpha (mStream.peek())))) + if (!isStringCharacter (c)) { putback (c); break; } - if (first && std::isdigit (c)) + if (first && (std::isdigit (c) || c=='`' || c=='-')) error = true; } @@ -499,9 +499,21 @@ namespace Compiler return true; } + bool Scanner::isStringCharacter (char c, bool lookAhead) + { + return std::isalpha (c) || std::isdigit (c) || c=='_' || + /// \todo disable this when doing more stricter compiling + c=='`' || + /// \todo disable this when doing more stricter compiling. Also, find out who is + /// responsible for allowing it in the first place and meet up with that person in + /// a dark alley. + (c=='-' && (!lookAhead || isStringCharacter (mStream.peek(), false))); + } + bool Scanner::isWhitespace (char c) { - return c==' ' || c=='\t'; + return c==' ' || c=='\t' + || c=='['; ///< \todo disable this when doing more strict compiling } // constructor @@ -509,7 +521,8 @@ namespace Compiler Scanner::Scanner (ErrorHandler& errorHandler, std::istream& inputStream, const Extensions *extensions) : mErrorHandler (errorHandler), mStream (inputStream), mExtensions (extensions), - mPutback (Putback_None), mPutbackCode(0), mPutbackInteger(0), mPutbackFloat(0) + mPutback (Putback_None), mPutbackCode(0), mPutbackInteger(0), mPutbackFloat(0), + mNameStartingWithDigit (false) { } @@ -561,4 +574,9 @@ namespace Compiler if (mExtensions) mExtensions->listKeywords (keywords); } + + void Scanner::allowNameStartingwithDigit() + { + mNameStartingWithDigit = true; + } } diff --git a/components/compiler/scanner.hpp b/components/compiler/scanner.hpp index 19f4ca96a..349a8afb6 100644 --- a/components/compiler/scanner.hpp +++ b/components/compiler/scanner.hpp @@ -37,6 +37,7 @@ namespace Compiler float mPutbackFloat; std::string mPutbackName; TokenLoc mPutbackLoc; + bool mNameStartingWithDigit; public: @@ -92,6 +93,8 @@ namespace Compiler bool scanSpecial (char c, Parser& parser, bool& cont); + bool isStringCharacter (char c, bool lookAhead = true); + static bool isWhitespace (char c); public: @@ -119,7 +122,10 @@ namespace Compiler ///< put back a keyword token void listKeywords (std::vector& keywords); - ///< Append all known keywords to \Ʀ kaywords. + ///< Append all known keywords to \a kaywords. + + /// For the next token allow names to start with a digit. + void allowNameStartingwithDigit(); }; } diff --git a/components/compiler/scriptparser.hpp b/components/compiler/scriptparser.hpp index 000244c79..edabb9c5c 100644 --- a/components/compiler/scriptparser.hpp +++ b/components/compiler/scriptparser.hpp @@ -27,7 +27,7 @@ namespace Compiler bool end = false); void getCode (std::vector& code) const; - ///< store generated code in \Ʀ code. + ///< store generated code in \a code. virtual bool parseName (const std::string& name, const TokenLoc& loc, Scanner& scanner); diff --git a/components/contentselector/model/contentmodel.cpp b/components/contentselector/model/contentmodel.cpp index 0d274474c..0d4f2365a 100644 --- a/components/contentselector/model/contentmodel.cpp +++ b/components/contentselector/model/contentmodel.cpp @@ -23,6 +23,7 @@ ContentSelectorModel::ContentModel::ContentModel(QObject *parent) : void ContentSelectorModel::ContentModel::setEncoding(const QString &encoding) { + mEncoding = encoding; if (encoding == QLatin1String("win1252")) mCodec = QTextCodec::codecForName("windows-1252"); @@ -76,7 +77,7 @@ const ContentSelectorModel::EsmFile *ContentSelectorModel::ContentModel::item(co foreach (const EsmFile *file, mFiles) { - if (name == file->fileProperty (fp).toString()) + if (name.compare(file->fileProperty (fp).toString(), Qt::CaseInsensitive) == 0) return file; } return 0; @@ -119,7 +120,7 @@ Qt::ItemFlags ContentSelectorModel::ContentModel::flags(const QModelIndex &index { //compare filenames only. Multiple instances //of the filename (with different paths) is not relevant here. - depFound = (dependency->fileName() == fileName); + depFound = (dependency->fileName().compare(fileName, Qt::CaseInsensitive) == 0); if (!depFound) continue; @@ -239,7 +240,7 @@ bool ContentSelectorModel::ContentModel::setData(const QModelIndex &index, const return false; EsmFile *file = item(index.row()); - QString fileName = file->filePath(); + QString fileName = file->fileName(); bool success = false; switch(role) @@ -266,7 +267,7 @@ bool ContentSelectorModel::ContentModel::setData(const QModelIndex &index, const if (success) { - success = setCheckState(fileName, value.toBool()); + success = setCheckState(file->filePath(), value.toBool()); emit dataChanged(index, index); } } @@ -275,21 +276,20 @@ bool ContentSelectorModel::ContentModel::setData(const QModelIndex &index, const case Qt::CheckStateRole: { int checkValue = value.toInt(); - bool success = false; bool setState = false; - if ((checkValue==Qt::Checked) && !isChecked(fileName)) + if ((checkValue==Qt::Checked) && !isChecked(file->filePath())) { setState = true; success = true; } - else if ((checkValue == Qt::Checked) && isChecked (fileName)) + else if ((checkValue == Qt::Checked) && isChecked (file->filePath())) setState = true; else if (checkValue == Qt::Unchecked) setState = true; if (setState) { - setCheckState(fileName, success); + setCheckState(file->filePath(), success); emit dataChanged(index, index); } @@ -299,7 +299,7 @@ bool ContentSelectorModel::ContentModel::setData(const QModelIndex &index, const foreach (EsmFile *file, mFiles) { - if (file->gameFiles().contains(fileName)) + if (file->gameFiles().contains(fileName, Qt::CaseInsensitive)) { QModelIndex idx = indexFromItem(file); emit dataChanged(idx, idx); @@ -449,7 +449,7 @@ void ContentSelectorModel::ContentModel::addFiles(const QString &path) try { ESM::ESMReader fileReader; ToUTF8::Utf8Encoder encoder = - ToUTF8::calculateEncoding(QString(mCodec->name()).toStdString()); + ToUTF8::calculateEncoding(mEncoding.toStdString()); fileReader.setEncoder(&encoder); fileReader.open(dir.absoluteFilePath(path).toStdString()); @@ -500,7 +500,7 @@ void ContentSelectorModel::ContentModel::sortFiles() //dependencies appear. for (int j = i + 1; j < fileCount; j++) { - if (gamefiles.contains(mFiles.at(j)->fileName())) + if (gamefiles.contains(mFiles.at(j)->fileName(), Qt::CaseInsensitive)) { mFiles.move(j, i); @@ -517,10 +517,10 @@ void ContentSelectorModel::ContentModel::sortFiles() } } -bool ContentSelectorModel::ContentModel::isChecked(const QString& name) const +bool ContentSelectorModel::ContentModel::isChecked(const QString& filepath) const { - if (mCheckStates.contains(name)) - return (mCheckStates[name] == Qt::Checked); + if (mCheckStates.contains(filepath)) + return (mCheckStates[filepath] == Qt::Checked); return false; } @@ -543,12 +543,12 @@ void ContentSelectorModel::ContentModel::refreshModel() emit dataChanged (index(0,0), index(rowCount()-1,0)); } -bool ContentSelectorModel::ContentModel::setCheckState(const QString &name, bool checkState) +bool ContentSelectorModel::ContentModel::setCheckState(const QString &filepath, bool checkState) { - if (name.isEmpty()) + if (filepath.isEmpty()) return false; - const EsmFile *file = item(name); + const EsmFile *file = item(filepath); if (!file) return false; @@ -558,8 +558,8 @@ bool ContentSelectorModel::ContentModel::setCheckState(const QString &name, bool if (checkState) state = Qt::Checked; - mCheckStates[name] = state; - emit dataChanged(indexFromItem(item(name)), indexFromItem(item(name))); + mCheckStates[filepath] = state; + emit dataChanged(indexFromItem(item(filepath)), indexFromItem(item(filepath))); if (file->isGameFile()) refreshModel(); @@ -586,7 +586,10 @@ bool ContentSelectorModel::ContentModel::setCheckState(const QString &name, bool { foreach (const EsmFile *downstreamFile, mFiles) { - if (downstreamFile->gameFiles().contains(name)) + QFileInfo fileInfo(filepath); + QString filename = fileInfo.fileName(); + + if (downstreamFile->gameFiles().contains(filename, Qt::CaseInsensitive)) { if (mCheckStates.contains(downstreamFile->filePath())) mCheckStates[downstreamFile->filePath()] = Qt::Unchecked; diff --git a/components/contentselector/model/contentmodel.hpp b/components/contentselector/model/contentmodel.hpp index 8c8c2124b..7b2000b51 100644 --- a/components/contentselector/model/contentmodel.hpp +++ b/components/contentselector/model/contentmodel.hpp @@ -45,8 +45,8 @@ namespace ContentSelectorModel const EsmFile *item(const QString &name) const; bool isEnabled (QModelIndex index) const; - bool isChecked(const QString &name) const; - bool setCheckState(const QString &name, bool isChecked); + bool isChecked(const QString &filepath) const; + bool setCheckState(const QString &filepath, bool isChecked); void setCheckStates (const QStringList &fileList, bool isChecked); ContentFileList checkedItems() const; void uncheckAll(); @@ -64,6 +64,7 @@ namespace ContentSelectorModel ContentFileList mFiles; QHash mCheckStates; QTextCodec *mCodec; + QString mEncoding; public: diff --git a/components/contentselector/model/esmfile.hpp b/components/contentselector/model/esmfile.hpp index ca24b52d1..7e1edcaba 100644 --- a/components/contentselector/model/esmfile.hpp +++ b/components/contentselector/model/esmfile.hpp @@ -53,6 +53,8 @@ namespace ContentSelectorModel inline QDateTime modified() const { return mModified; } inline float format() const { return mFormat; } inline QString filePath() const { return mPath; } + + /// @note Contains file names, not paths. inline const QStringList &gameFiles() const { return mGameFiles; } inline QString description() const { return mDescription; } inline QString toolTip() const { return sToolTip.arg(mAuthor) diff --git a/components/esm/aipackage.cpp b/components/esm/aipackage.cpp index cf4951de7..209a1fe26 100644 --- a/components/esm/aipackage.cpp +++ b/components/esm/aipackage.cpp @@ -13,6 +13,7 @@ namespace ESM void AIPackageList::load(ESMReader &esm) { + mList.clear(); while (esm.hasMoreSubs()) { // initialize every iteration AIPackage pack; diff --git a/components/esm/aipackage.hpp b/components/esm/aipackage.hpp index 8a31aadf5..cbe82f16e 100644 --- a/components/esm/aipackage.hpp +++ b/components/esm/aipackage.hpp @@ -16,8 +16,10 @@ namespace ESM struct AIData { - // These are probabilities - char mHello, mU1, mFight, mFlee, mAlarm, mU2, mU3, mU4; + unsigned char mHello; + char mU1; + unsigned char mFight, mFlee, mAlarm; // These are probabilities [0, 100] + char mU2, mU3, mU4; // Unknown values int mServices; // See the Services enum void blank(); diff --git a/components/esm/aisequence.cpp b/components/esm/aisequence.cpp new file mode 100644 index 000000000..0e3e54102 --- /dev/null +++ b/components/esm/aisequence.cpp @@ -0,0 +1,214 @@ +#include "aisequence.hpp" + +#include "esmreader.hpp" +#include "esmwriter.hpp" + +#include "defs.hpp" + +#include + +namespace ESM +{ +namespace AiSequence +{ + + void AiWander::load(ESMReader &esm) + { + esm.getHNT (mData, "DATA"); + esm.getHNT(mStartTime, "STAR"); + } + + void AiWander::save(ESMWriter &esm) const + { + esm.writeHNT ("DATA", mData); + esm.writeHNT ("STAR", mStartTime); + } + + void AiTravel::load(ESMReader &esm) + { + esm.getHNT (mData, "DATA"); + } + + void AiTravel::save(ESMWriter &esm) const + { + esm.writeHNT ("DATA", mData); + } + + void AiEscort::load(ESMReader &esm) + { + esm.getHNT (mData, "DATA"); + mTargetId = esm.getHNString("TARG"); + esm.getHNT (mRemainingDuration, "DURA"); + mCellId = esm.getHNOString ("CELL"); + } + + void AiEscort::save(ESMWriter &esm) const + { + esm.writeHNT ("DATA", mData); + esm.writeHNString ("TARG", mTargetId); + esm.writeHNT ("DURA", mRemainingDuration); + if (!mCellId.empty()) + esm.writeHNString ("CELL", mCellId); + } + + void AiFollow::load(ESMReader &esm) + { + esm.getHNT (mData, "DATA"); + mTargetId = esm.getHNString("TARG"); + esm.getHNT (mRemainingDuration, "DURA"); + mCellId = esm.getHNOString ("CELL"); + esm.getHNT (mAlwaysFollow, "ALWY"); + mCommanded = false; + esm.getHNOT (mCommanded, "CMND"); + } + + void AiFollow::save(ESMWriter &esm) const + { + esm.writeHNT ("DATA", mData); + esm.writeHNString("TARG", mTargetId); + esm.writeHNT ("DURA", mRemainingDuration); + if (!mCellId.empty()) + esm.writeHNString ("CELL", mCellId); + esm.writeHNT ("ALWY", mAlwaysFollow); + esm.writeHNT ("CMND", mCommanded); + } + + void AiActivate::load(ESMReader &esm) + { + mTargetId = esm.getHNString("TARG"); + } + + void AiActivate::save(ESMWriter &esm) const + { + esm.writeHNString("TARG", mTargetId); + } + + void AiCombat::load(ESMReader &esm) + { + esm.getHNT (mTargetActorId, "TARG"); + } + + void AiCombat::save(ESMWriter &esm) const + { + esm.writeHNT ("TARG", mTargetActorId); + } + + void AiPursue::load(ESMReader &esm) + { + esm.getHNT (mTargetActorId, "TARG"); + } + + void AiPursue::save(ESMWriter &esm) const + { + esm.writeHNT ("TARG", mTargetActorId); + } + + AiSequence::~AiSequence() + { + for (std::vector::iterator it = mPackages.begin(); it != mPackages.end(); ++it) + delete it->mPackage; + } + + void AiSequence::save(ESMWriter &esm) const + { + for (std::vector::const_iterator it = mPackages.begin(); it != mPackages.end(); ++it) + { + esm.writeHNT ("AIPK", it->mType); + switch (it->mType) + { + case Ai_Wander: + static_cast(it->mPackage)->save(esm); + break; + case Ai_Travel: + static_cast(it->mPackage)->save(esm); + break; + case Ai_Escort: + static_cast(it->mPackage)->save(esm); + break; + case Ai_Follow: + static_cast(it->mPackage)->save(esm); + break; + case Ai_Activate: + static_cast(it->mPackage)->save(esm); + break; + case Ai_Combat: + static_cast(it->mPackage)->save(esm); + break; + case Ai_Pursue: + static_cast(it->mPackage)->save(esm); + break; + + default: + break; + } + } + } + + void AiSequence::load(ESMReader &esm) + { + while (esm.isNextSub("AIPK")) + { + int type; + esm.getHT(type); + + mPackages.push_back(AiPackageContainer()); + mPackages.back().mType = type; + + switch (type) + { + case Ai_Wander: + { + std::auto_ptr ptr (new AiWander()); + ptr->load(esm); + mPackages.back().mPackage = ptr.release(); + break; + } + case Ai_Travel: + { + std::auto_ptr ptr (new AiTravel()); + ptr->load(esm); + mPackages.back().mPackage = ptr.release(); + break; + } + case Ai_Escort: + { + std::auto_ptr ptr (new AiEscort()); + ptr->load(esm); + mPackages.back().mPackage = ptr.release(); + break; + } + case Ai_Follow: + { + std::auto_ptr ptr (new AiFollow()); + ptr->load(esm); + mPackages.back().mPackage = ptr.release(); + break; + } + case Ai_Activate: + { + std::auto_ptr ptr (new AiActivate()); + ptr->load(esm); + mPackages.back().mPackage = ptr.release(); + break; + } + case Ai_Combat: + { + std::auto_ptr ptr (new AiCombat()); + ptr->load(esm); + mPackages.back().mPackage = ptr.release(); + break; + } + case Ai_Pursue: + { + std::auto_ptr ptr (new AiPursue()); + ptr->load(esm); + mPackages.back().mPackage = ptr.release(); + break; + } + default: + return; + } + } + } +} +} diff --git a/components/esm/aisequence.hpp b/components/esm/aisequence.hpp new file mode 100644 index 000000000..da16bf867 --- /dev/null +++ b/components/esm/aisequence.hpp @@ -0,0 +1,155 @@ +#ifndef OPENMW_COMPONENTS_ESM_AISEQUENCE_H +#define OPENMW_COMPONENTS_ESM_AISEQUENCE_H + +#include +#include + +#include "defs.hpp" + +namespace ESM +{ + class ESMReader; + class ESMWriter; + + namespace AiSequence + { + + // format 0, saved games only + // As opposed to AiPackageList, this stores the "live" version of AI packages. + + enum AiPackages + { + Ai_Wander = ESM::FourCC<'W','A','N','D'>::value, + Ai_Travel = ESM::FourCC<'T','R','A','V'>::value, + Ai_Escort = ESM::FourCC<'E','S','C','O'>::value, + Ai_Follow = ESM::FourCC<'F','O','L','L'>::value, + Ai_Activate = ESM::FourCC<'A','C','T','I'>::value, + Ai_Combat = ESM::FourCC<'C','O','M','B'>::value, + Ai_Pursue = ESM::FourCC<'P','U','R','S'>::value + }; + + + struct AiPackage + { + virtual ~AiPackage() {} + }; + + +#pragma pack(push,1) + struct AiWanderData + { + short mDistance; + short mDuration; + unsigned char mTimeOfDay; + unsigned char mIdle[8]; + unsigned char mShouldRepeat; + }; + struct AiTravelData + { + float mX, mY, mZ; + }; + struct AiEscortData + { + float mX, mY, mZ; + short mDuration; + }; + +#pragma pack(pop) + + struct AiWander : AiPackage + { + AiWanderData mData; + ESM::TimeStamp mStartTime; + + /// \todo add more AiWander state + + void load(ESMReader &esm); + void save(ESMWriter &esm) const; + }; + + struct AiTravel : AiPackage + { + AiTravelData mData; + + void load(ESMReader &esm); + void save(ESMWriter &esm) const; + }; + + struct AiEscort : AiPackage + { + AiEscortData mData; + + std::string mTargetId; + std::string mCellId; + float mRemainingDuration; + + void load(ESMReader &esm); + void save(ESMWriter &esm) const; + }; + + struct AiFollow : AiPackage + { + AiEscortData mData; + + std::string mTargetId; + std::string mCellId; + float mRemainingDuration; + + bool mAlwaysFollow; + bool mCommanded; + + void load(ESMReader &esm); + void save(ESMWriter &esm) const; + }; + + struct AiActivate : AiPackage + { + std::string mTargetId; + + void load(ESMReader &esm); + void save(ESMWriter &esm) const; + }; + + struct AiCombat : AiPackage + { + int mTargetActorId; + + void load(ESMReader &esm); + void save(ESMWriter &esm) const; + }; + + struct AiPursue : AiPackage + { + int mTargetActorId; + + void load(ESMReader &esm); + void save(ESMWriter &esm) const; + }; + + struct AiPackageContainer + { + int mType; + + AiPackage* mPackage; + }; + + struct AiSequence + { + AiSequence() {} + ~AiSequence(); + + std::vector mPackages; + + void load (ESMReader &esm); + void save (ESMWriter &esm) const; + + private: + AiSequence(const AiSequence&); + AiSequence& operator=(const AiSequence&); + }; + + } + +} + +#endif diff --git a/components/esm/cellref.cpp b/components/esm/cellref.cpp index 84518bed9..29d26d013 100644 --- a/components/esm/cellref.cpp +++ b/components/esm/cellref.cpp @@ -6,10 +6,12 @@ void ESM::CellRef::load (ESMReader& esm, bool wideRefNum) { - // NAM0 sometimes appears here, sometimes further on - mNam0 = 0; + // According to Hrnchamd, this does not belong to the actual ref. Instead, it is a marker indicating that + // the following refs are part of a "temp refs" section. A temp ref is not being tracked by the moved references system. + // Its only purpose is a performance optimization for "immovable" things. We don't need this, and it's problematic anyway, + // because any item can theoretically be moved by a script. if (esm.isNextSub ("NAM0")) - esm.getHT (mNam0); + esm.skipHSub(); if (wideRefNum) esm.getHNT (mRefNum, "FRMR", 8); @@ -27,12 +29,12 @@ void ESM::CellRef::load (ESMReader& esm, bool wideRefNum) esm.getHNOT (mScale, "XSCL"); mOwner = esm.getHNOString ("ANAM"); - mGlob = esm.getHNOString ("BNAM"); + mGlobalVariable = esm.getHNOString ("BNAM"); mSoul = esm.getHNOString ("XSOL"); mFaction = esm.getHNOString ("CNAM"); - mFactIndex = -2; - esm.getHNOT (mFactIndex, "INDX"); + mFactionRank = -2; + esm.getHNOT (mFactionRank, "INDX"); mGoldValue = 1; mCharge = -1; @@ -60,20 +62,14 @@ void ESM::CellRef::load (ESMReader& esm, bool wideRefNum) mKey = esm.getHNOString ("KNAM"); mTrap = esm.getHNOString ("TNAM"); - mFltv = 0; esm.getHNOT (mReferenceBlocked, "UNAM"); - esm.getHNOT (mFltv, "FLTV"); + if (esm.isNextSub("FLTV")) // no longer used + esm.skipHSub(); esm.getHNOT(mPos, "DATA", 24); - // Number of references in the cell? Maximum once in each cell, - // but not always at the beginning, and not always right. In other - // words, completely useless. - // Update: Well, maybe not completely useless. This might actually be - // number_of_references + number_of_references_moved_here_Across_boundaries, - // and could be helpful for collecting these weird moved references. - if (esm.isNextSub ("NAM0")) - esm.getHT (mNam0); + if (esm.isNextSub("NAM0")) + esm.skipHSub(); } void ESM::CellRef::save (ESMWriter &esm, bool wideRefNum, bool inInventory) const @@ -90,12 +86,12 @@ void ESM::CellRef::save (ESMWriter &esm, bool wideRefNum, bool inInventory) cons } esm.writeHNOCString("ANAM", mOwner); - esm.writeHNOCString("BNAM", mGlob); + esm.writeHNOCString("BNAM", mGlobalVariable); esm.writeHNOCString("XSOL", mSoul); esm.writeHNOCString("CNAM", mFaction); - if (mFactIndex != -2) { - esm.writeHNT("INDX", mFactIndex); + if (mFactionRank != -2) { + esm.writeHNT("INDX", mFactionRank); } if (mEnchantmentCharge != -1) @@ -108,13 +104,13 @@ void ESM::CellRef::save (ESMWriter &esm, bool wideRefNum, bool inInventory) cons esm.writeHNT("NAM9", mGoldValue); } - if (mTeleport && !inInventory) + if (!inInventory && mTeleport) { esm.writeHNT("DODT", mDoorDest); esm.writeHNOCString("DNAM", mDestCell); } - if (mLockLevel != 0 && !inInventory) { + if (!inInventory && mLockLevel != 0) { esm.writeHNT("FLTV", mLockLevel); } @@ -127,14 +123,8 @@ void ESM::CellRef::save (ESMWriter &esm, bool wideRefNum, bool inInventory) cons if (mReferenceBlocked != -1) esm.writeHNT("UNAM", mReferenceBlocked); - if (mFltv != 0 && !inInventory) - esm.writeHNT("FLTV", mFltv); - if (!inInventory) esm.writeHNT("DATA", mPos, 24); - - if (mNam0 != 0 && !inInventory) - esm.writeHNT("NAM0", mNam0); } void ESM::CellRef::blank() @@ -144,10 +134,10 @@ void ESM::CellRef::blank() mRefID.clear(); mScale = 1; mOwner.clear(); - mGlob.clear(); + mGlobalVariable.clear(); mSoul.clear(); mFaction.clear(); - mFactIndex = -1; + mFactionRank = -2; mCharge = 0; mEnchantmentCharge = 0; mGoldValue = 0; @@ -156,8 +146,7 @@ void ESM::CellRef::blank() mKey.clear(); mTrap.clear(); mReferenceBlocked = 0; - mFltv = 0; - mNam0 = 0; + mTeleport = false; for (int i=0; i<3; ++i) { @@ -172,3 +161,14 @@ bool ESM::operator== (const RefNum& left, const RefNum& right) { return left.mIndex==right.mIndex && left.mContentFile==right.mContentFile; } + +bool ESM::operator< (const RefNum& left, const RefNum& right) +{ + if (left.mIndexright.mIndex) + return false; + + return left.mContentFile::const_iterator it = mSummonedCreatureMap.begin(); it != mSummonedCreatureMap.end(); ++it) + { + esm.writeHNT ("SUMM", it->first); + esm.writeHNT ("ACID", it->second); + } + + for (std::vector::const_iterator it = mSummonGraveyard.begin(); it != mSummonGraveyard.end(); ++it) + { + esm.writeHNT ("GRAV", *it); + } + + esm.writeHNT("AISE", mHasAiSettings); + for (int i=0; i<4; ++i) + mAiSettings[i].save(esm); } diff --git a/components/esm/creaturestats.hpp b/components/esm/creaturestats.hpp index 5ca3d071f..8f4d4df7b 100644 --- a/components/esm/creaturestats.hpp +++ b/components/esm/creaturestats.hpp @@ -11,6 +11,8 @@ #include "spellstate.hpp" #include "activespells.hpp" +#include "magiceffects.hpp" +#include "aisequence.hpp" namespace ESM { @@ -23,17 +25,27 @@ namespace ESM StatState mAttributes[8]; StatState mDynamic[3]; + MagicEffects mMagicEffects; + + AiSequence::AiSequence mAiSequence; + + bool mHasAiSettings; + StatState mAiSettings[4]; + + std::map mSummonedCreatureMap; + std::vector mSummonGraveyard; + ESM::TimeStamp mTradeTime; int mGoldPool; int mActorId; bool mDead; bool mDied; + bool mMurdered; int mFriendlyHits; bool mTalkedTo; bool mAlarmed; bool mAttacked; - bool mHostile; bool mAttackingOrSpell; bool mKnockdown; bool mKnockdownOneFrame; diff --git a/components/esm/debugprofile.cpp b/components/esm/debugprofile.cpp new file mode 100644 index 000000000..6c05fac2a --- /dev/null +++ b/components/esm/debugprofile.cpp @@ -0,0 +1,29 @@ + +#include "debugprofile.hpp" + +#include "esmreader.hpp" +#include "esmwriter.hpp" +#include "defs.hpp" + +unsigned int ESM::DebugProfile::sRecordId = REC_DBGP; + +void ESM::DebugProfile::load (ESMReader& esm) +{ + mDescription = esm.getHNString ("DESC"); + mScriptText = esm.getHNString ("SCRP"); + esm.getHNT (mFlags, "FLAG"); +} + +void ESM::DebugProfile::save (ESMWriter& esm) const +{ + esm.writeHNCString ("DESC", mDescription); + esm.writeHNCString ("SCRP", mScriptText); + esm.writeHNT ("FLAG", mFlags); +} + +void ESM::DebugProfile::blank() +{ + mDescription.clear(); + mScriptText.clear(); + mFlags = 0; +} diff --git a/components/esm/debugprofile.hpp b/components/esm/debugprofile.hpp new file mode 100644 index 000000000..b54e8ff5f --- /dev/null +++ b/components/esm/debugprofile.hpp @@ -0,0 +1,38 @@ +#ifndef COMPONENTS_ESM_DEBUGPROFILE_H +#define COMPONENTS_ESM_DEBUGPROFILE_H + +#include + +namespace ESM +{ + class ESMReader; + class ESMWriter; + + struct DebugProfile + { + static unsigned int sRecordId; + + enum Flags + { + Flag_Default = 1, // add to newly opened scene subviews + Flag_BypassNewGame = 2, // bypass regular game startup + Flag_Global = 4 // make available from main menu (i.e. not location specific) + }; + + std::string mId; + + std::string mDescription; + + std::string mScriptText; + + unsigned int mFlags; + + void load (ESMReader& esm); + void save (ESMWriter& esm) const; + + /// Set record to default state (does not touch the ID). + void blank(); + }; +} + +#endif diff --git a/components/esm/defs.hpp b/components/esm/defs.hpp index bdeb95291..d5ba919b0 100644 --- a/components/esm/defs.hpp +++ b/components/esm/defs.hpp @@ -111,9 +111,13 @@ enum RecNameInts REC_ACTC = FourCC<'A','C','T','C'>::value, REC_MPRJ = FourCC<'M','P','R','J'>::value, REC_PROJ = FourCC<'P','R','O','J'>::value, + REC_DCOU = FourCC<'D','C','O','U'>::value, + REC_MARK = FourCC<'M','A','R','K'>::value, + REC_ENAB = FourCC<'E','N','A','B'>::value, // format 1 - REC_FILT = 0x544C4946 + REC_FILT = 0x544C4946, + REC_DBGP = FourCC<'D','B','G','P'>::value ///< only used in project files }; } diff --git a/components/esm/effectlist.cpp b/components/esm/effectlist.cpp index bc126846b..fc9acccf2 100644 --- a/components/esm/effectlist.cpp +++ b/components/esm/effectlist.cpp @@ -7,6 +7,7 @@ namespace ESM { void EffectList::load(ESMReader &esm) { + mList.clear(); ENAMstruct s; while (esm.isNextSub("ENAM")) { esm.getHT(s, 24); diff --git a/components/esm/esmreader.cpp b/components/esm/esmreader.cpp index ebdb1e41f..6facee381 100644 --- a/components/esm/esmreader.cpp +++ b/components/esm/esmreader.cpp @@ -250,14 +250,6 @@ void ESMReader::skipRecord() mCtx.leftRec = 0; } -void ESMReader::skipHRecord() -{ - if (!mCtx.leftFile) - return; - getRecHeader(); - skipRecord(); -} - void ESMReader::getRecHeader(uint32_t &flags) { // General error checking diff --git a/components/esm/esmreader.hpp b/components/esm/esmreader.hpp index 6b0bb9a27..1549e15f5 100644 --- a/components/esm/esmreader.hpp +++ b/components/esm/esmreader.hpp @@ -83,7 +83,7 @@ public: // indirectly to the load() method. int mIdx; void setIndex(const int index) {mIdx = index; mCtx.index = index;} - const int getIndex() {return mIdx;} + int getIndex() {return mIdx;} void setGlobalReaderList(std::vector *list) {mGlobalReaderList = list;} std::vector *getGlobalReaderList() {return mGlobalReaderList;} @@ -216,9 +216,6 @@ public: // already been read void skipRecord(); - // Skip an entire record, including the header (but not the name) - void skipHRecord(); - /* Read record header. This updatesleftFile BEYOND the data that follows the header, ie beyond the entire record. You should use leftRec to orient yourself inside the record itself. diff --git a/components/esm/esmwriter.cpp b/components/esm/esmwriter.cpp index 9d8d943d9..544f8bbed 100644 --- a/components/esm/esmwriter.cpp +++ b/components/esm/esmwriter.cpp @@ -6,7 +6,12 @@ namespace ESM { - ESMWriter::ESMWriter() : mEncoder (0), mRecordCount (0), mCounting (true) {} + ESMWriter::ESMWriter() + : mEncoder (0) + , mRecordCount (0) + , mCounting (true) + , mStream(NULL) + {} unsigned int ESMWriter::getVersion() const { @@ -18,6 +23,11 @@ namespace ESM mHeader.mData.version = ver; } + void ESMWriter::setType(int type) + { + mHeader.mData.type = type; + } + void ESMWriter::setAuthor(const std::string& auth) { mHeader.mData.author.assign (auth); diff --git a/components/esm/esmwriter.hpp b/components/esm/esmwriter.hpp index ca4f42217..e57c6e45d 100644 --- a/components/esm/esmwriter.hpp +++ b/components/esm/esmwriter.hpp @@ -25,10 +25,15 @@ class ESMWriter ESMWriter(); unsigned int getVersion() const; + + // Set various header data (ESM::Header::Data). All of the below functions must be called before writing, + // otherwise this data will be left uninitialized. void setVersion(unsigned int ver = 0x3fa66666); + void setType(int type); void setEncoder(ToUTF8::Utf8Encoder *encoding); void setAuthor(const std::string& author); void setDescription(const std::string& desc); + // Set the record count for writing it in the file header void setRecordCount (int count); // Counts how many records we have actually written. diff --git a/components/esm/globalmap.cpp b/components/esm/globalmap.cpp index f17c071ff..190329c61 100644 --- a/components/esm/globalmap.cpp +++ b/components/esm/globalmap.cpp @@ -21,7 +21,7 @@ void ESM::GlobalMap::load (ESMReader &esm) CellId cell; esm.getT(cell.first); esm.getT(cell.second); - mMarkers.push_back(cell); + mMarkers.insert(cell); } } @@ -33,7 +33,7 @@ void ESM::GlobalMap::save (ESMWriter &esm) const esm.write(&mImageData[0], mImageData.size()); esm.endRecord("DATA"); - for (std::vector::const_iterator it = mMarkers.begin(); it != mMarkers.end(); ++it) + for (std::set::const_iterator it = mMarkers.begin(); it != mMarkers.end(); ++it) { esm.startSubRecord("MRK_"); esm.writeT(it->first); diff --git a/components/esm/globalmap.hpp b/components/esm/globalmap.hpp index 158f70a6e..e89123f89 100644 --- a/components/esm/globalmap.hpp +++ b/components/esm/globalmap.hpp @@ -2,6 +2,7 @@ #define OPENMW_COMPONENTS_ESM_GLOBALMAP_H #include +#include namespace ESM { @@ -26,7 +27,7 @@ namespace ESM std::vector mImageData; typedef std::pair CellId; - std::vector mMarkers; + std::set mMarkers; void load (ESMReader &esm); void save (ESMWriter &esm) const; diff --git a/components/esm/globalscript.cpp b/components/esm/globalscript.cpp index dcbd91140..467fe54a1 100644 --- a/components/esm/globalscript.cpp +++ b/components/esm/globalscript.cpp @@ -12,6 +12,8 @@ void ESM::GlobalScript::load (ESMReader &esm) mRunning = 0; esm.getHNOT (mRunning, "RUN_"); + + mTargetId = esm.getHNOString ("TARG"); } void ESM::GlobalScript::save (ESMWriter &esm) const @@ -22,4 +24,6 @@ void ESM::GlobalScript::save (ESMWriter &esm) const if (mRunning) esm.writeHNT ("RUN_", mRunning); + + esm.writeHNOString ("TARG", mTargetId); } \ No newline at end of file diff --git a/components/esm/globalscript.hpp b/components/esm/globalscript.hpp index 4fb8b7c48..43c859e09 100644 --- a/components/esm/globalscript.hpp +++ b/components/esm/globalscript.hpp @@ -15,6 +15,7 @@ namespace ESM std::string mId; Locals mLocals; int mRunning; + std::string mTargetId; // for targeted scripts void load (ESMReader &esm); void save (ESMWriter &esm) const; diff --git a/components/esm/inventorystate.cpp b/components/esm/inventorystate.cpp index 1b0bc772e..2154faa83 100644 --- a/components/esm/inventorystate.cpp +++ b/components/esm/inventorystate.cpp @@ -37,6 +37,8 @@ void ESM::InventoryState::load (ESMReader &esm) LightState state; int slot; read (esm, state, slot); + if (state.mCount == 0) + continue; mLights.push_back (std::make_pair (state, slot)); } else @@ -44,6 +46,8 @@ void ESM::InventoryState::load (ESMReader &esm) ObjectState state; int slot; read (esm, state, slot); + if (state.mCount == 0) + continue; mItems.push_back (std::make_pair (state, std::make_pair (id, slot))); } } diff --git a/components/esm/loadalch.cpp b/components/esm/loadalch.cpp index f6bfc6a11..aac88482f 100644 --- a/components/esm/loadalch.cpp +++ b/components/esm/loadalch.cpp @@ -10,7 +10,7 @@ namespace ESM void Potion::load(ESMReader &esm) { - mModel = esm.getHNString("MODL"); + mModel = esm.getHNOString("MODL"); mIcon = esm.getHNOString("TEXT"); // not ITEX here for some reason mScript = esm.getHNOString("SCRI"); mName = esm.getHNOString("FNAM"); diff --git a/components/esm/loadarmo.cpp b/components/esm/loadarmo.cpp index 5bf38c840..f8c3a4718 100644 --- a/components/esm/loadarmo.cpp +++ b/components/esm/loadarmo.cpp @@ -9,6 +9,7 @@ namespace ESM void PartReferenceList::load(ESMReader &esm) { + mParts.clear(); while (esm.isNextSub("INDX")) { PartReference pr; diff --git a/components/esm/loadarmo.hpp b/components/esm/loadarmo.hpp index 991f4e185..6be9dd971 100644 --- a/components/esm/loadarmo.hpp +++ b/components/esm/loadarmo.hpp @@ -46,7 +46,7 @@ enum PartReferenceType // Reference to body parts struct PartReference { - char mPart; + unsigned char mPart; // possible values [0, 26] std::string mMale, mFemale; }; diff --git a/components/esm/loadbody.cpp b/components/esm/loadbody.cpp index c45f8d252..9a1164d04 100644 --- a/components/esm/loadbody.cpp +++ b/components/esm/loadbody.cpp @@ -22,4 +22,14 @@ void BodyPart::save(ESMWriter &esm) const esm.writeHNT("BYDT", mData, 4); } + void BodyPart::blank() + { + mData.mPart = 0; + mData.mVampire = 0; + mData.mFlags = 0; + mData.mType = 0; + + mModel.clear(); + mRace.clear(); + } } diff --git a/components/esm/loadbody.hpp b/components/esm/loadbody.hpp index 9623caa31..5e9869d24 100644 --- a/components/esm/loadbody.hpp +++ b/components/esm/loadbody.hpp @@ -49,10 +49,10 @@ struct BodyPart struct BYDTstruct { - char mPart; - char mVampire; - char mFlags; - char mType; + unsigned char mPart; // mesh part + unsigned char mVampire; // boolean + unsigned char mFlags; + unsigned char mType; // mesh type }; BYDTstruct mData; @@ -60,6 +60,9 @@ struct BodyPart void load(ESMReader &esm); void save(ESMWriter &esm) const; + + void blank(); + ///< Set record to default state (does not touch the ID). }; } #endif diff --git a/components/esm/loadcell.cpp b/components/esm/loadcell.cpp index 0830c5de6..bbd6696f1 100644 --- a/components/esm/loadcell.cpp +++ b/components/esm/loadcell.cpp @@ -50,19 +50,15 @@ namespace ESM return ref.mRefNum == refNum; } - void Cell::load(ESMReader &esm, bool saveContext) { - // Ignore this for now, it might mean we should delete the entire - // cell? - // TODO: treat the special case "another plugin moved this ref, but we want to delete it"! - if (esm.isNextSub("DELE")) { - esm.skipHSub(); - } - - esm.getHNT(mData, "DATA", 12); + loadData(esm); + loadCell(esm, saveContext); +} - mNAM0 = 0; +void Cell::loadCell(ESMReader &esm, bool saveContext) +{ + mRefNumCounter = 0; if (mData.mFlags & Interior) { @@ -73,12 +69,10 @@ void Cell::load(ESMReader &esm, bool saveContext) esm.getHT(waterl); mWater = (float) waterl; mWaterInt = true; - mHasWaterLevelRecord = true; } else if (esm.isNextSub("WHGT")) { esm.getHT(mWater); - mHasWaterLevelRecord = true; } // Quasi-exterior cells have a region (which determines the @@ -98,7 +92,7 @@ void Cell::load(ESMReader &esm, bool saveContext) esm.getHNOT(mMapColor, "NAM5"); } if (esm.isNextSub("NAM0")) { - esm.getHT(mNAM0); + esm.getHT(mRefNumCounter); } if (saveContext) { @@ -107,9 +101,16 @@ void Cell::load(ESMReader &esm, bool saveContext) } } -void Cell::preLoad(ESMReader &esm) //Can't be "load" because it conflicts with function in esmtool +void Cell::loadData(ESMReader &esm) { - this->load(esm, false); + // Ignore this for now, it might mean we should delete the entire + // cell? + // TODO: treat the special case "another plugin moved this ref, but we want to delete it"! + if (esm.isNextSub("DELE")) { + esm.skipHSub(); + } + + esm.getHNT(mData, "DATA", 12); } void Cell::postLoad(ESMReader &esm) @@ -124,14 +125,12 @@ void Cell::save(ESMWriter &esm) const esm.writeHNT("DATA", mData, 12); if (mData.mFlags & Interior) { - if (mHasWaterLevelRecord) { - if (mWaterInt) { - int water = - (mWater >= 0) ? (int) (mWater + 0.5) : (int) (mWater - 0.5); - esm.writeHNT("INTV", water); - } else { - esm.writeHNT("WHGT", mWater); - } + if (mWaterInt) { + int water = + (mWater >= 0) ? (int) (mWater + 0.5) : (int) (mWater - 0.5); + esm.writeHNT("INTV", water); + } else { + esm.writeHNT("WHGT", mWater); } if (mData.mFlags & QuasiEx) @@ -146,8 +145,8 @@ void Cell::save(ESMWriter &esm) const esm.writeHNT("NAM5", mMapColor); } - if (mNAM0 != 0) - esm.writeHNT("NAM0", mNAM0); + if (mRefNumCounter != 0) + esm.writeHNT("NAM0", mRefNumCounter); } void Cell::restore(ESMReader &esm, int iCtx) const @@ -216,7 +215,7 @@ bool Cell::getNextMVRF(ESMReader &esm, MovedCellRef &mref) mWater = 0; mWaterInt = false; mMapColor = 0; - mNAM0 = 0; + mRefNumCounter = 0; mData.mFlags = 0; mData.mX = 0; @@ -228,19 +227,6 @@ bool Cell::getNextMVRF(ESMReader &esm, MovedCellRef &mref) mAmbi.mFogDensity = 0; } - void Cell::merge(Cell *original, Cell *modified) - { - float waterLevel = original->mWater; - if (modified->mHasWaterLevelRecord) - { - waterLevel = modified->mWater; - } - // else: keep original water level, instead of resetting to 0 - - *original = *modified; - original->mWater = waterLevel; - } - CellId Cell::getCellId() const { CellId id; diff --git a/components/esm/loadcell.hpp b/components/esm/loadcell.hpp index 28204c9ee..a24b106d4 100644 --- a/components/esm/loadcell.hpp +++ b/components/esm/loadcell.hpp @@ -78,10 +78,7 @@ struct Cell float mFogDensity; }; - Cell() : mWater(0), mHasWaterLevelRecord(false) {} - - /// Merge \a modified into \a original - static void merge (Cell* original, Cell* modified); + Cell() : mWater(0) {} // Interior cells are indexed by this (it's the 'id'), for exterior // cells it is optional. @@ -93,23 +90,28 @@ struct Cell std::vector mContextList; // File position; multiple positions for multiple plugin support DATAstruct mData; AMBIstruct mAmbi; + float mWater; // Water level - bool mHasWaterLevelRecord; bool mWaterInt; int mMapColor; - int mNAM0; + // Counter for RefNums. This is only used during content file editing and has no impact on gameplay. + // It prevents overwriting previous refNums, even if they were deleted. + // as that would collide with refs when a content file is upgraded. + int mRefNumCounter; // References "leased" from another cell (i.e. a different cell // introduced this ref, and it has been moved here by a plugin) CellRefTracker mLeasedRefs; MovedCellRefTracker mMovedRefs; - void preLoad(ESMReader &esm); void postLoad(ESMReader &esm); // This method is left in for compatibility with esmtool. Parsing moved references currently requires // passing ESMStore, bit it does not know about this parameter, so we do it this way. - void load(ESMReader &esm, bool saveContext = true); + void load(ESMReader &esm, bool saveContext = true); // Load everything (except references) + void loadData(ESMReader &esm); // Load DATAstruct only + void loadCell(ESMReader &esm, bool saveContext = true); // Load everything, except DATAstruct and references + void save(ESMWriter &esm) const; bool isExterior() const diff --git a/components/esm/loadclot.cpp b/components/esm/loadclot.cpp index d64564d77..17ecdf3ae 100644 --- a/components/esm/loadclot.cpp +++ b/components/esm/loadclot.cpp @@ -8,33 +8,34 @@ namespace ESM { unsigned int Clothing::sRecordId = REC_CLOT; -void Clothing::load(ESMReader &esm) -{ - mModel = esm.getHNString("MODL"); - mName = esm.getHNOString("FNAM"); - esm.getHNT(mData, "CTDT", 12); + void Clothing::load(ESMReader &esm) + { + mModel = esm.getHNString("MODL"); + mName = esm.getHNOString("FNAM"); + esm.getHNT(mData, "CTDT", 12); - mScript = esm.getHNOString("SCRI"); - mIcon = esm.getHNOString("ITEX"); + mScript = esm.getHNOString("SCRI"); + mIcon = esm.getHNOString("ITEX"); - mParts.load(esm); + mParts.load(esm); - mEnchant = esm.getHNOString("ENAM"); -} -void Clothing::save(ESMWriter &esm) const -{ - esm.writeHNCString("MODL", mModel); - esm.writeHNOCString("FNAM", mName); - esm.writeHNT("CTDT", mData, 12); + mEnchant = esm.getHNOString("ENAM"); + } - esm.writeHNOCString("SCRI", mScript); - esm.writeHNOCString("ITEX", mIcon); + void Clothing::save(ESMWriter &esm) const + { + esm.writeHNCString("MODL", mModel); + esm.writeHNOCString("FNAM", mName); + esm.writeHNT("CTDT", mData, 12); - mParts.save(esm); + esm.writeHNOCString("SCRI", mScript); + esm.writeHNOCString("ITEX", mIcon); - esm.writeHNOCString("ENAM", mEnchant); -} + mParts.save(esm); + + esm.writeHNOCString("ENAM", mEnchant); + } void Clothing::blank() { @@ -46,7 +47,6 @@ void Clothing::save(ESMWriter &esm) const mName.clear(); mModel.clear(); mIcon.clear(); - mIcon.clear(); mEnchant.clear(); mScript.clear(); } diff --git a/components/esm/loadcont.cpp b/components/esm/loadcont.cpp index 7bdf9f05b..51a385f06 100644 --- a/components/esm/loadcont.cpp +++ b/components/esm/loadcont.cpp @@ -9,6 +9,7 @@ namespace ESM void InventoryList::load(ESMReader &esm) { + mList.clear(); ContItem ci; while (esm.isNextSub("NPCO")) { diff --git a/components/esm/loaddial.cpp b/components/esm/loaddial.cpp index ee7ddbfad..ff0362aa2 100644 --- a/components/esm/loaddial.cpp +++ b/components/esm/loaddial.cpp @@ -40,21 +40,21 @@ void Dialogue::save(ESMWriter &esm) const } } - void Dialogue::blank() - { - mInfo.clear(); - } +void Dialogue::blank() +{ + mInfo.clear(); +} -void Dialogue::addInfo(const ESM::DialInfo& info, bool merge) +void Dialogue::readInfo(ESMReader &esm, bool merge) { - if (!merge || mInfo.empty() || info.mNext.empty()) - { - mLookup[info.mId] = mInfo.insert(mInfo.end(), info); - return; - } - if (info.mPrev.empty()) + const std::string& id = esm.getHNOString("INAM"); + + if (!merge || mInfo.empty()) { - mLookup[info.mId] = mInfo.insert(mInfo.begin(), info); + ESM::DialInfo info; + info.mId = id; + info.load(esm); + mLookup[id] = mInfo.insert(mInfo.end(), info); return; } @@ -62,11 +62,30 @@ void Dialogue::addInfo(const ESM::DialInfo& info, bool merge) std::map::iterator lookup; - lookup = mLookup.find(info.mId); + lookup = mLookup.find(id); if (lookup != mLookup.end()) { it = lookup->second; - *it = info; + + // Merge with existing record. Only the subrecords that are present in + // the new record will be overwritten. + it->load(esm); + return; + } + + // New record + ESM::DialInfo info; + info.mId = id; + info.load(esm); + + if (info.mNext.empty()) + { + mLookup[id] = mInfo.insert(mInfo.end(), info); + return; + } + if (info.mPrev.empty()) + { + mLookup[id] = mInfo.insert(mInfo.begin(), info); return; } @@ -75,7 +94,7 @@ void Dialogue::addInfo(const ESM::DialInfo& info, bool merge) { it = lookup->second; - mLookup[info.mId] = mInfo.insert(++it, info); + mLookup[id] = mInfo.insert(++it, info); return; } @@ -84,11 +103,22 @@ void Dialogue::addInfo(const ESM::DialInfo& info, bool merge) { it = lookup->second; - mLookup[info.mId] = mInfo.insert(it, info); + mLookup[id] = mInfo.insert(it, info); return; } - std::cerr << "Failed to insert info " << info.mId << std::endl; + std::cerr << "Failed to insert info " << id << std::endl; +} + +void Dialogue::clearDeletedInfos() +{ + for (InfoContainer::iterator it = mInfo.begin(); it != mInfo.end(); ) + { + if (it->mQuestStatus == DialInfo::QS_Deleted) + it = mInfo.erase(it); + else + ++it; + } } } diff --git a/components/esm/loaddial.hpp b/components/esm/loaddial.hpp index 6ec5527f9..d29948c63 100644 --- a/components/esm/loaddial.hpp +++ b/components/esm/loaddial.hpp @@ -47,8 +47,12 @@ struct Dialogue void load(ESMReader &esm); void save(ESMWriter &esm) const; + /// Remove all INFOs marked as QS_Deleted from mInfos. + void clearDeletedInfos(); + + /// Read the next info record /// @param merge Merge with existing list, or just push each record to the end of the list? - void addInfo (const ESM::DialInfo& info, bool merge); + void readInfo (ESM::ESMReader& esm, bool merge); void blank(); ///< Set record to default state (does not touch the ID and does not change the type). diff --git a/components/esm/loadench.cpp b/components/esm/loadench.cpp index a1e885f23..243803833 100644 --- a/components/esm/loadench.cpp +++ b/components/esm/loadench.cpp @@ -20,4 +20,13 @@ void Enchantment::save(ESMWriter &esm) const mEffects.save(esm); } + void Enchantment::blank() + { + mData.mType = 0; + mData.mCost = 0; + mData.mCharge = 0; + mData.mAutocalc = 0; + + mEffects.mList.clear(); + } } diff --git a/components/esm/loadench.hpp b/components/esm/loadench.hpp index f6ba8c6ab..3b7746812 100644 --- a/components/esm/loadench.hpp +++ b/components/esm/loadench.hpp @@ -42,6 +42,9 @@ struct Enchantment void load(ESMReader &esm); void save(ESMWriter &esm) const; + + void blank(); + ///< Set record to default state (does not touch the ID). }; } #endif diff --git a/components/esm/loadfact.cpp b/components/esm/loadfact.cpp index 0924efb17..db7e5b7b4 100644 --- a/components/esm/loadfact.cpp +++ b/components/esm/loadfact.cpp @@ -12,7 +12,7 @@ namespace ESM int& Faction::FADTstruct::getSkill (int index, bool ignored) { - if (index<0 || index>=6) + if (index<0 || index>=7) throw std::logic_error ("skill index out of range"); return mSkills[index]; @@ -20,7 +20,7 @@ namespace ESM int Faction::FADTstruct::getSkill (int index, bool ignored) const { - if (index<0 || index>=6) + if (index<0 || index>=7) throw std::logic_error ("skill index out of range"); return mSkills[index]; @@ -75,7 +75,6 @@ void Faction::save(ESMWriter &esm) const { mName.clear(); mData.mAttribute[0] = mData.mAttribute[1] = 0; - mData.mUnknown = -1; mData.mIsHidden = 0; for (int i=0; i<10; ++i) @@ -87,7 +86,7 @@ void Faction::save(ESMWriter &esm) const mRanks[i].clear(); } - for (int i=0; i<6; ++i) + for (int i=0; i<7; ++i) mData.mSkills[i] = 0; mReactions.clear(); diff --git a/components/esm/loadfact.hpp b/components/esm/loadfact.hpp index 75e30a5bf..d31670fe2 100644 --- a/components/esm/loadfact.hpp +++ b/components/esm/loadfact.hpp @@ -40,8 +40,9 @@ struct Faction RankData mRankData[10]; - int mSkills[6]; // IDs of skills this faction require - int mUnknown; // Always -1? + int mSkills[7]; // IDs of skills this faction require + // Each element will either contain an ESM::Skill index, or -1. + int mIsHidden; // 1 - hidden from player int& getSkill (int index, bool ignored = false); diff --git a/components/esm/loadinfo.cpp b/components/esm/loadinfo.cpp index f86ad3b51..a2bade1c5 100644 --- a/components/esm/loadinfo.cpp +++ b/components/esm/loadinfo.cpp @@ -10,14 +10,23 @@ namespace ESM void DialInfo::load(ESMReader &esm) { + mQuestStatus = QS_None; + mFactionLess = false; + mPrev = esm.getHNString("PNAM"); mNext = esm.getHNString("NNAM"); + // Since there's no way to mark selects as "deleted", we have to clear the SelectStructs from all previous loadings + mSelects.clear(); + // Not present if deleted if (esm.isNextSub("DATA")) { esm.getHT(mData, 12); } + if (!esm.hasMoreSubs()) + return; + // What follows is somewhat spaghetti-ish, but it's worth if for // an extra speedup. INFO is by far the most common record type. @@ -46,7 +55,6 @@ void DialInfo::load(ESMReader &esm) return; } - mFactionLess = false; if (subName.val == REC_FNAM) { mFaction = esm.getHString(); @@ -101,8 +109,6 @@ void DialInfo::load(ESMReader &esm) return; } - mQuestStatus = QS_None; - if (subName.val == REC_QSTN) mQuestStatus = QS_Name; else if (subName.val == REC_QSTF) diff --git a/components/esm/loadland.cpp b/components/esm/loadland.cpp index 1b701229e..91b062596 100644 --- a/components/esm/loadland.cpp +++ b/components/esm/loadland.cpp @@ -19,14 +19,14 @@ void Land::LandData::save(ESMWriter &esm) offsets.mUnk1 = mUnk1; offsets.mUnk2 = mUnk2; - float prevY = mHeights[0], prevX; + float prevY = mHeights[0]; int number = 0; // avoid multiplication for (int i = 0; i < LAND_SIZE; ++i) { float diff = (mHeights[number] - prevY) / HEIGHT_SCALE; offsets.mHeightData[number] = (diff >= 0) ? (int8_t) (diff + 0.5) : (int8_t) (diff - 0.5); - prevX = prevY = mHeights[number]; + float prevX = prevY = mHeights[number]; ++number; for (int j = 1; j < LAND_SIZE; ++j) { diff --git a/components/esm/loadland.hpp b/components/esm/loadland.hpp index 028341ced..002066981 100644 --- a/components/esm/loadland.hpp +++ b/components/esm/loadland.hpp @@ -85,6 +85,11 @@ struct Land char mColours[3 * LAND_NUM_VERTS]; int mDataTypes; + // WNAM appears to contain the global map image for this cell. Probably a palette-based format, + // since there's only 1 byte for each pixel. + // Currently unused (global map is drawn on the fly in OpenMW, takes ~1/2 second at startup for Morrowind.esm). + // The problem with using the original data is that we would need to exactly replicate the TES CS's algorithm + // for drawing the global map in OpenCS, in order to get seamless edges when creating landmass mods. uint8_t mWnam[81]; short mUnk1; uint8_t mUnk2; @@ -98,6 +103,8 @@ struct Land void load(ESMReader &esm); void save(ESMWriter &esm) const; + void blank() {} + /** * Actually loads data */ diff --git a/components/esm/loadligh.cpp b/components/esm/loadligh.cpp index c02bb46b6..f88ff09d6 100644 --- a/components/esm/loadligh.cpp +++ b/components/esm/loadligh.cpp @@ -10,7 +10,7 @@ namespace ESM void Light::load(ESMReader &esm) { - mModel = esm.getHNString("MODL"); + mModel = esm.getHNOString("MODL"); mName = esm.getHNOString("FNAM"); mIcon = esm.getHNOString("ITEX"); assert(sizeof(mData) == 24); diff --git a/components/esm/loadligh.hpp b/components/esm/loadligh.hpp index 74eb37197..ffc291609 100644 --- a/components/esm/loadligh.hpp +++ b/components/esm/loadligh.hpp @@ -25,7 +25,7 @@ struct Light Negative = 0x004, // Negative light - i.e. darkness Flicker = 0x008, Fire = 0x010, - OffDefault = 0x020, // Off by default + OffDefault = 0x020, // Off by default - does not burn while placed in a cell, but can burn when equipped by an NPC FlickerSlow = 0x040, Pulse = 0x080, PulseSlow = 0x100 diff --git a/components/esm/loadltex.cpp b/components/esm/loadltex.cpp index bd28c8488..c3e2d50ff 100644 --- a/components/esm/loadltex.cpp +++ b/components/esm/loadltex.cpp @@ -19,4 +19,10 @@ void LandTexture::save(ESMWriter &esm) const esm.writeHNCString("DATA", mTexture); } +void LandTexture::blank() +{ + mTexture.clear(); + mIndex = -1; +} + } diff --git a/components/esm/loadltex.hpp b/components/esm/loadltex.hpp index 5e84428b2..8b45f8211 100644 --- a/components/esm/loadltex.hpp +++ b/components/esm/loadltex.hpp @@ -32,6 +32,9 @@ struct LandTexture std::string mId, mTexture; int mIndex; + void blank(); + ///< Set record to default state (does not touch the ID). + void load(ESMReader &esm); void save(ESMWriter &esm) const; }; diff --git a/components/esm/loadmgef.cpp b/components/esm/loadmgef.cpp index f60191539..cbdca3e31 100644 --- a/components/esm/loadmgef.cpp +++ b/components/esm/loadmgef.cpp @@ -1,6 +1,7 @@ #include "loadmgef.hpp" #include +#include #include @@ -10,6 +11,157 @@ namespace { + static const char *sIds[ESM::MagicEffect::Length] = + { + "WaterBreathing", + "SwiftSwim", + "WaterWalking", + "Shield", + "FireShield", + "LightningShield", + "FrostShield", + "Burden", + "Feather", + "Jump", + "Levitate", + "SlowFall", + "Lock", + "Open", + "FireDamage", + "ShockDamage", + "FrostDamage", + "DrainAttribute", + "DrainHealth", + "DrainMagicka", + "DrainFatigue", + "DrainSkill", + "DamageAttribute", + "DamageHealth", + "DamageMagicka", + "DamageFatigue", + "DamageSkill", + "Poison", + "WeaknessToFire", + "WeaknessToFrost", + "WeaknessToShock", + "WeaknessToMagicka", + "WeaknessToCommonDisease", + "WeaknessToBlightDisease", + "WeaknessToCorprusDisease", + "WeaknessToPoison", + "WeaknessToNormalWeapons", + "DisintegrateWeapon", + "DisintegrateArmor", + "Invisibility", + "Chameleon", + "Light", + "Sanctuary", + "NightEye", + "Charm", + "Paralyze", + "Silence", + "Blind", + "Sound", + "CalmHumanoid", + "CalmCreature", + "FrenzyHumanoid", + "FrenzyCreature", + "DemoralizeHumanoid", + "DemoralizeCreature", + "RallyHumanoid", + "RallyCreature", + "Dispel", + "Soultrap", + "Telekinesis", + "Mark", + "Recall", + "DivineIntervention", + "AlmsiviIntervention", + "DetectAnimal", + "DetectEnchantment", + "DetectKey", + "SpellAbsorption", + "Reflect", + "CureCommonDisease", + "CureBlightDisease", + "CureCorprusDisease", + "CurePoison", + "CureParalyzation", + "RestoreAttribute", + "RestoreHealth", + "RestoreMagicka", + "RestoreFatigue", + "RestoreSkill", + "FortifyAttribute", + "FortifyHealth", + "FortifyMagicka", + "FortifyFatigue", + "FortifySkill", + "FortifyMaximumMagicka", + "AbsorbAttribute", + "AbsorbHealth", + "AbsorbMagicka", + "AbsorbFatigue", + "AbsorbSkill", + "ResistFire", + "ResistFrost", + "ResistShock", + "ResistMagicka", + "ResistCommonDisease", + "ResistBlightDisease", + "ResistCorprusDisease", + "ResistPoison", + "ResistNormalWeapons", + "ResistParalysis", + "RemoveCurse", + "TurnUndead", + "SummonScamp", + "SummonClannfear", + "SummonDaedroth", + "SummonDremora", + "SummonAncestralGhost", + "SummonSkeletalMinion", + "SummonBonewalker", + "SummonGreaterBonewalker", + "SummonBonelord", + "SummonWingedTwilight", + "SummonHunger", + "SummonGoldenSaint", + "SummonFlameAtronach", + "SummonFrostAtronach", + "SummonStormAtronach", + "FortifyAttack", + "CommandCreature", + "CommandHumanoid", + "BoundDagger", + "BoundLongsword", + "BoundMace", + "BoundBattleAxe", + "BoundSpear", + "BoundLongbow", + "ExtraSpell", + "BoundCuirass", + "BoundHelm", + "BoundBoots", + "BoundShield", + "BoundGloves", + "Corprus", + "Vampirism", + "SummonCenturionSphere", + "SunDamage", + "StuntedMagicka", + + // Tribunal only + "SummonFabricant", + + // Bloodmoon only + "SummonWolf", + "SummonBear", + "SummonBonewolf", + "SummonCreature04", + "SummonCreature05" + }; + const int NumberOfHardcodedFlags = 143; const int HardcodedFlags[NumberOfHardcodedFlags] = { 0x11c8, 0x11c0, 0x11c8, 0x11e0, 0x11e0, 0x11e0, 0x11e0, 0x11d0, @@ -41,9 +193,16 @@ void MagicEffect::load(ESMReader &esm) { esm.getHNT(mIndex, "INDX"); + mId = indexToId (mIndex); + esm.getHNT(mData, "MEDT", 36); - if (mIndex>=0 && mIndex=0 && mIndex=0 && index sNames; @@ -86,6 +97,8 @@ struct MagicEffect void load(ESMReader &esm); void save(ESMWriter &esm) const; + /// Set record to default state (does not touch the ID/index). + void blank(); enum Effects { @@ -239,6 +252,8 @@ struct MagicEffect Length }; + + static std::string indexToId (int index); }; } #endif diff --git a/components/esm/loadnpc.cpp b/components/esm/loadnpc.cpp index e5b851bf0..ef4b5211b 100644 --- a/components/esm/loadnpc.cpp +++ b/components/esm/loadnpc.cpp @@ -10,8 +10,6 @@ namespace ESM void NPC::load(ESMReader &esm) { - //mNpdt52.mGold = -10; - mPersistent = esm.getRecordFlags() & 0x0400; mModel = esm.getHNOString("MODL"); @@ -53,6 +51,7 @@ void NPC::load(ESMReader &esm) else mHasAI = false; + mTransport.clear(); while (esm.isNextSub("DODT") || esm.isNextSub("DNAM")) { if (esm.retSubName() == 0x54444f44) { // DODT struct Dest dodt; @@ -63,7 +62,6 @@ void NPC::load(ESMReader &esm) } } mAiPackage.load(esm); - esm.skipRecord(); } void NPC::save(ESMWriter &esm) const { diff --git a/components/esm/loadnpc.hpp b/components/esm/loadnpc.hpp index 08f678b45..0e90108c3 100644 --- a/components/esm/loadnpc.hpp +++ b/components/esm/loadnpc.hpp @@ -80,10 +80,12 @@ struct NPC mPersonality, mLuck; - char mSkills[Skill::Length]; - char mReputation; - short mHealth, mMana, mFatigue; - char mDisposition, mFactionID, mRank; + // mSkill can grow up to 200, it must be unsigned + unsigned char mSkills[Skill::Length]; + + char mFactionID; + unsigned short mHealth, mMana, mFatigue; + signed char mDisposition, mReputation, mRank; char mUnknown; int mGold; }; // 52 bytes @@ -91,9 +93,10 @@ struct NPC struct NPDTstruct12 { short mLevel; - char mDisposition, mReputation, mRank; + // see above + signed char mDisposition, mReputation, mRank; char mUnknown1, mUnknown2, mUnknown3; - int mGold; // ?? not certain + int mGold; }; // 12 bytes struct Dest @@ -103,7 +106,7 @@ struct NPC }; #pragma pack(pop) - char mNpdtType; + unsigned char mNpdtType; NPDTstruct52 mNpdt52; NPDTstruct12 mNpdt12; //for autocalculated characters diff --git a/components/esm/loadpgrd.cpp b/components/esm/loadpgrd.cpp index efdbdd86b..7fdc9a43c 100644 --- a/components/esm/loadpgrd.cpp +++ b/components/esm/loadpgrd.cpp @@ -27,6 +27,9 @@ void Pathgrid::load(ESMReader &esm) esm.getHNT(mData, "DATA", 12); mCell = esm.getHNString("NAME"); + mPoints.clear(); + mEdges.clear(); + // keep track of total connections so we can reserve edge vector size int edgeCount = 0; @@ -112,4 +115,14 @@ void Pathgrid::save(ESMWriter &esm) const } } + void Pathgrid::blank() + { + mCell.clear(); + mData.mX = 0; + mData.mY = 0; + mData.mS1 = 0; + mData.mS2 = 0; + mPoints.clear(); + mEdges.clear(); + } } diff --git a/components/esm/loadpgrd.hpp b/components/esm/loadpgrd.hpp index 60a736991..256b86cda 100644 --- a/components/esm/loadpgrd.hpp +++ b/components/esm/loadpgrd.hpp @@ -53,6 +53,8 @@ struct Pathgrid void load(ESMReader &esm); void save(ESMWriter &esm) const; + + void blank(); }; } #endif diff --git a/components/esm/loadregn.cpp b/components/esm/loadregn.cpp index da03e009f..1b08b7217 100644 --- a/components/esm/loadregn.cpp +++ b/components/esm/loadregn.cpp @@ -12,10 +12,26 @@ void Region::load(ESMReader &esm) { mName = esm.getHNOString("FNAM"); + esm.getSubNameIs("WEAT"); + esm.getSubHeader(); if (esm.getVer() == VER_12) - esm.getHNExact(&mData, sizeof(mData) - 2, "WEAT"); + { + mData.mA = 0; + mData.mB = 0; + esm.getExact(&mData, sizeof(mData) - 2); + } else if (esm.getVer() == VER_13) - esm.getHNExact(&mData, sizeof(mData), "WEAT"); + { + // May include the additional two bytes (but not necessarily) + if (esm.getSubSize() == sizeof(mData)) + esm.getExact(&mData, sizeof(mData)); + else + { + mData.mA = 0; + mData.mB = 0; + esm.getExact(&mData, sizeof(mData)-2); + } + } else esm.fail("Don't know what to do in this version"); @@ -23,6 +39,7 @@ void Region::load(ESMReader &esm) esm.getHNT(mMapColor, "CNAM"); + mSoundList.clear(); while (esm.hasMoreSubs()) { SoundRef sr; diff --git a/components/esm/loadregn.hpp b/components/esm/loadregn.hpp index 1992c951b..c231b6aa0 100644 --- a/components/esm/loadregn.hpp +++ b/components/esm/loadregn.hpp @@ -24,10 +24,11 @@ struct Region #pragma pack(1) struct WEATstruct { - // I guess these are probabilities - char mClear, mCloudy, mFoggy, mOvercast, mRain, mThunder, mAsh, mBlight, + // These are probabilities that add up to 100 + unsigned char mClear, mCloudy, mFoggy, mOvercast, mRain, mThunder, mAsh, mBlight, // Unknown weather, probably snow and something. Only // present in file version 1.3. + // the engine uses mA as "snow" and mB as "blizard" mA, mB; }; // 10 bytes @@ -35,7 +36,7 @@ struct Region struct SoundRef { NAME32 mSound; - char mChance; + unsigned char mChance; }; // 33 bytes #pragma pack(pop) diff --git a/components/esm/loadscpt.cpp b/components/esm/loadscpt.cpp index aee8628bd..19e3f3bb3 100644 --- a/components/esm/loadscpt.cpp +++ b/components/esm/loadscpt.cpp @@ -22,6 +22,8 @@ void Script::load(ESMReader &esm) mData = data.mData; mId = data.mName.toString(); + mVarNames.clear(); + // List of local variables if (esm.isNextSub("SCVR")) { @@ -38,22 +40,51 @@ void Script::load(ESMReader &esm) char* str = &tmp[0]; for (size_t i = 0; i < mVarNames.size(); i++) { + // Support '\r' terminated strings like vanilla. See Bug #1324. char *termsym = strchr(str, '\r'); if(termsym) *termsym = '\0'; mVarNames[i] = std::string(str); str += mVarNames[i].size() + 1; if (str - &tmp[0] > s) - esm.fail("String table overflow"); + { + // Apparently SCVR subrecord is not used and variable names are + // determined on the fly from the script text. Therefore don't throw + // an exeption, just log an error and continue. + std::stringstream ss; + + ss << "ESM Error: " << "String table overflow"; + ss << "\n File: " << esm.getName(); + ss << "\n Record: " << esm.getContext().recName.toString(); + ss << "\n Subrecord: " << "SCVR"; + ss << "\n Offset: 0x" << std::hex << esm.getFileOffset(); + std::cerr << ss.str() << std::endl; + break; + } + } } // Script mData - mScriptData.resize(mData.mScriptDataSize); - esm.getHNExact(&mScriptData[0], mScriptData.size(), "SCDT"); + if (esm.isNextSub("SCDT")) + { + mScriptData.resize(mData.mScriptDataSize); + esm.getHExact(&mScriptData[0], mScriptData.size()); + } // Script text mScriptText = esm.getHNOString("SCTX"); + + // NOTE: A minor hack/workaround... + // + // MAO_Containers.esp from Morrowind Acoustic Overhaul has SCVR records + // at the end (see Bug #1849). Since OpenMW does not use SCVR subrecords + // for variable names just skip these as a quick fix. An alternative + // solution would be to decode and validate SCVR subrecords even if they + // appear here. + if (esm.isNextSub("SCVR")) { + esm.skipHSub(); + } } void Script::save(ESMWriter &esm) const { @@ -95,7 +126,11 @@ void Script::save(ESMWriter &esm) const mVarNames.clear(); mScriptData.clear(); - mScriptText = "Begin " + mId + "\n\nEnd " + mId + "\n"; + + if (mId.find ("::")!=std::string::npos) + mScriptText = "Begin \"" + mId + "\"\n\nEnd " + mId + "\n"; + else + mScriptText = "Begin " + mId + "\n\nEnd " + mId + "\n"; } } diff --git a/components/esm/loadscpt.hpp b/components/esm/loadscpt.hpp index d5200d4c1..38160e7f4 100644 --- a/components/esm/loadscpt.hpp +++ b/components/esm/loadscpt.hpp @@ -23,29 +23,8 @@ public: struct SCHDstruct { - /* Script name. - - NOTE: You should handle the name "Main" (case insensitive) with - care. With tribunal, modders got the ability to add 'start - scripts' to their mods, which is a script that is run at - startup and which runs throughout the game (I think.) - - However, before Tribunal, there was only one startup script, - called "Main". If mods wanted to make their own start scripts, - they had to overwrite Main. This is obviously problem if - multiple mods to this at the same time. - - Although most mods have switched to using Trib-style startup - scripts, some legacy mods might still overwrite Main, and this - can cause problems if several mods do it. I think the best - course of action is to NEVER overwrite main, but instead add - each with a separate unique name and add them to the start - script list. But there might be other problems with this - approach though. - */ - - // These describe the sizes we need to allocate for the script - // data. + /// Data from script-precompling in the editor. + /// \warning Do not use them. OpenCS currently does not precompile scripts. int mNumShorts, mNumLongs, mNumFloats, mScriptDataSize, mStringTableSize; }; // 52 bytes @@ -53,9 +32,16 @@ public: SCHDstruct mData; - std::vector mVarNames; // Variable names - std::vector mScriptData; // Compiled bytecode - std::string mScriptText; // Uncompiled script + /// Variable names generated by script-precompiling in the editor. + /// \warning Do not use this field. OpenCS currently does not precompile scripts. + std::vector mVarNames; + + /// Bytecode generated from script-precompiling in the editor. + /// \warning Do not use this field. OpenCS currently does not precompile scripts. + std::vector mScriptData; + + /// Script source code + std::string mScriptText; void load(ESMReader &esm); void save(ESMWriter &esm) const; diff --git a/components/esm/loadsndg.cpp b/components/esm/loadsndg.cpp index 1a8ca6335..9ab061ec2 100644 --- a/components/esm/loadsndg.cpp +++ b/components/esm/loadsndg.cpp @@ -22,4 +22,10 @@ void SoundGenerator::save(ESMWriter &esm) const esm.writeHNOCString("SNAM", mSound); } + void SoundGenerator::blank() + { + mType = LeftFoot; + mCreature.clear(); + mSound.clear(); + } } diff --git a/components/esm/loadsndg.hpp b/components/esm/loadsndg.hpp index 5509661c1..f89a11208 100644 --- a/components/esm/loadsndg.hpp +++ b/components/esm/loadsndg.hpp @@ -36,6 +36,8 @@ struct SoundGenerator void load(ESMReader &esm); void save(ESMWriter &esm) const; + + void blank(); }; } #endif diff --git a/components/esm/loadspel.hpp b/components/esm/loadspel.hpp index cbf5366c4..4bd2210ec 100644 --- a/components/esm/loadspel.hpp +++ b/components/esm/loadspel.hpp @@ -27,8 +27,8 @@ struct Spell enum Flags { - F_Autocalc = 1, - F_PCStart = 2, + F_Autocalc = 1, // Can be selected by NPC spells auto-calc + F_PCStart = 2, // Can be selected by player spells auto-calc F_Always = 4 // Casting always succeeds }; diff --git a/components/esm/loadstat.cpp b/components/esm/loadstat.cpp index a71f22dc2..53d1b4bb5 100644 --- a/components/esm/loadstat.cpp +++ b/components/esm/loadstat.cpp @@ -10,6 +10,7 @@ namespace ESM void Static::load(ESMReader &esm) { + mPersistent = esm.getRecordFlags() & 0x0400; mModel = esm.getHNString("MODL"); } void Static::save(ESMWriter &esm) const diff --git a/components/esm/loadstat.hpp b/components/esm/loadstat.hpp index d912d1058..45b05136a 100644 --- a/components/esm/loadstat.hpp +++ b/components/esm/loadstat.hpp @@ -26,6 +26,8 @@ struct Static std::string mId, mModel; + bool mPersistent; + void load(ESMReader &esm); void save(ESMWriter &esm) const; diff --git a/components/esm/magiceffects.cpp b/components/esm/magiceffects.cpp new file mode 100644 index 000000000..898e7e4b1 --- /dev/null +++ b/components/esm/magiceffects.cpp @@ -0,0 +1,29 @@ +#include "magiceffects.hpp" + +#include "esmwriter.hpp" +#include "esmreader.hpp" + +namespace ESM +{ + +void MagicEffects::save(ESMWriter &esm) const +{ + for (std::map::const_iterator it = mEffects.begin(); it != mEffects.end(); ++it) + { + esm.writeHNT("EFID", it->first); + esm.writeHNT("BASE", it->second); + } +} + +void MagicEffects::load(ESMReader &esm) +{ + while (esm.isNextSub("EFID")) + { + int id, base; + esm.getHT(id); + esm.getHNT(base, "BASE"); + mEffects.insert(std::make_pair(id, base)); + } +} + +} diff --git a/components/esm/magiceffects.hpp b/components/esm/magiceffects.hpp new file mode 100644 index 000000000..2a6052caa --- /dev/null +++ b/components/esm/magiceffects.hpp @@ -0,0 +1,23 @@ +#ifndef COMPONENTS_ESM_MAGICEFFECTS_H +#define COMPONENTS_ESM_MAGICEFFECTS_H + +#include + +namespace ESM +{ + class ESMReader; + class ESMWriter; + + // format 0, saved games only + struct MagicEffects + { + // + std::map mEffects; + + void load (ESMReader &esm); + void save (ESMWriter &esm) const; + }; + +} + +#endif diff --git a/components/esm/npcstats.cpp b/components/esm/npcstats.cpp index 3fa954182..3e6aed99d 100644 --- a/components/esm/npcstats.cpp +++ b/components/esm/npcstats.cpp @@ -4,7 +4,7 @@ #include "esmreader.hpp" #include "esmwriter.hpp" -ESM::NpcStats::Faction::Faction() : mExpelled (false), mRank (0), mReputation (0) {} +ESM::NpcStats::Faction::Faction() : mExpelled (false), mRank (-1), mReputation (0) {} void ESM::NpcStats::load (ESMReader &esm) { @@ -78,8 +78,9 @@ void ESM::NpcStats::load (ESMReader &esm) mLastDrowningHit = 0; esm.getHNOT (mLastDrowningHit, "DRLH"); - mLevelHealthBonus = 0; - esm.getHNOT (mLevelHealthBonus, "LVLH"); + // No longer used + float levelHealthBonus = 0; + esm.getHNOT (levelHealthBonus, "LVLH"); mCrimeId = -1; esm.getHNOT (mCrimeId, "CRID"); @@ -98,7 +99,7 @@ void ESM::NpcStats::save (ESMWriter &esm) const esm.writeHNT ("FAEX", expelled); } - if (iter->second.mRank) + if (iter->second.mRank >= 0) esm.writeHNT ("FARA", iter->second.mRank); if (iter->second.mReputation) @@ -148,9 +149,6 @@ void ESM::NpcStats::save (ESMWriter &esm) const if (mLastDrowningHit) esm.writeHNT ("DRLH", mLastDrowningHit); - if (mLevelHealthBonus) - esm.writeHNT ("LVLH", mLevelHealthBonus); - if (mCrimeId != -1) esm.writeHNT ("CRID", mCrimeId); } diff --git a/components/esm/npcstats.hpp b/components/esm/npcstats.hpp index ce7c75d2a..0061fc05f 100644 --- a/components/esm/npcstats.hpp +++ b/components/esm/npcstats.hpp @@ -46,7 +46,6 @@ namespace ESM std::vector mUsedIds; float mTimeToStartDrowning; float mLastDrowningHit; - float mLevelHealthBonus; int mCrimeId; void load (ESMReader &esm); diff --git a/components/esm/spelllist.cpp b/components/esm/spelllist.cpp index 24d3c3d0a..8ec386db4 100644 --- a/components/esm/spelllist.cpp +++ b/components/esm/spelllist.cpp @@ -7,6 +7,7 @@ namespace ESM { void SpellList::load(ESMReader &esm) { + mList.clear(); while (esm.isNextSub("NPCS")) { mList.push_back(esm.getHString()); } @@ -19,4 +20,12 @@ void SpellList::save(ESMWriter &esm) const } } +bool SpellList::exists(const std::string &spell) const +{ + for (std::vector::const_iterator it = mList.begin(); it != mList.end(); ++it) + if (Misc::StringUtils::ciEqual(*it, spell)) + return true; + return false; +} + } diff --git a/components/esm/spelllist.hpp b/components/esm/spelllist.hpp index 934bdda7a..bcd6ba798 100644 --- a/components/esm/spelllist.hpp +++ b/components/esm/spelllist.hpp @@ -16,6 +16,9 @@ namespace ESM { std::vector mList; + /// Is this spell ID in mList? + bool exists(const std::string& spell) const; + void load(ESMReader &esm); void save(ESMWriter &esm) const; }; diff --git a/components/esm/spellstate.cpp b/components/esm/spellstate.cpp index 2dca2dcec..3ed3329b4 100644 --- a/components/esm/spellstate.cpp +++ b/components/esm/spellstate.cpp @@ -27,6 +27,34 @@ namespace ESM mSpells[id] = random; } + while (esm.isNextSub("PERM")) + { + std::string spellId = esm.getHString(); + + std::vector permEffectList; + while (esm.isNextSub("EFID")) + { + PermanentSpellEffectInfo info; + esm.getHT(info.mId); + esm.getHNT(info.mArg, "ARG_"); + esm.getHNT(info.mMagnitude, "MAGN"); + + permEffectList.push_back(info); + } + mPermanentSpellEffects[spellId] = permEffectList; + } + + while (esm.isNextSub("CORP")) + { + std::string id = esm.getHString(); + + CorprusStats stats; + esm.getHNT(stats.mWorsenings, "WORS"); + esm.getHNT(stats.mNextWorsening, "TIME"); + + mCorprusSpells[id] = stats; + } + while (esm.isNextSub("USED")) { std::string id = esm.getHString(); @@ -53,6 +81,28 @@ namespace ESM } } + for (std::map >::const_iterator it = mPermanentSpellEffects.begin(); it != mPermanentSpellEffects.end(); ++it) + { + esm.writeHNString("PERM", it->first); + + const std::vector & effects = it->second; + for (std::vector::const_iterator effectIt = effects.begin(); effectIt != effects.end(); ++effectIt) + { + esm.writeHNT("EFID", effectIt->mId); + esm.writeHNT("ARG_", effectIt->mArg); + esm.writeHNT("MAGN", effectIt->mMagnitude); + } + } + + for (std::map::const_iterator it = mCorprusSpells.begin(); it != mCorprusSpells.end(); ++it) + { + esm.writeHNString("CORP", it->first); + + const CorprusStats & stats = it->second; + esm.writeHNT("WORS", stats.mWorsenings); + esm.writeHNT("TIME", stats.mNextWorsening); + } + for (std::map::const_iterator it = mUsedPowers.begin(); it != mUsedPowers.end(); ++it) { esm.writeHNString("USED", it->first); diff --git a/components/esm/spellstate.hpp b/components/esm/spellstate.hpp index cb5c0ff0d..2ab27e908 100644 --- a/components/esm/spellstate.hpp +++ b/components/esm/spellstate.hpp @@ -2,6 +2,7 @@ #define OPENMW_ESM_SPELLSTATE_H #include +#include #include #include "defs.hpp" @@ -13,9 +14,26 @@ namespace ESM struct SpellState { + struct CorprusStats + { + int mWorsenings; + TimeStamp mNextWorsening; + }; + + struct PermanentSpellEffectInfo + { + int mId; + int mArg; + float mMagnitude; + }; + typedef std::map > TContainer; TContainer mSpells; + std::map > mPermanentSpellEffects; + + std::map mCorprusSpells; + std::map mUsedPowers; std::string mSelectedSpell; diff --git a/components/esm/statstate.hpp b/components/esm/statstate.hpp index 4b4023bc2..801d0ce82 100644 --- a/components/esm/statstate.hpp +++ b/components/esm/statstate.hpp @@ -12,9 +12,10 @@ namespace ESM struct StatState { T mBase; - T mMod; + T mMod; // Note: can either be the modifier, or the modified value. + // A bit inconsistent, but we can't fix this without breaking compatibility. T mCurrent; - T mDamage; + float mDamage; float mProgress; StatState(); @@ -30,11 +31,19 @@ namespace ESM void StatState::load (ESMReader &esm) { esm.getHNT (mBase, "STBA"); - esm.getHNT (mMod, "STMO"); + + mMod = 0; + esm.getHNOT (mMod, "STMO"); mCurrent = 0; esm.getHNOT (mCurrent, "STCU"); - mDamage = 0; - esm.getHNOT (mDamage, "STDA"); + + // mDamage was changed to a float; ensure backwards compatibility + T oldDamage = 0; + esm.getHNOT(oldDamage, "STDA"); + mDamage = oldDamage; + + esm.getHNOT (mDamage, "STDF"); + mProgress = 0; esm.getHNOT (mProgress, "STPR"); } @@ -43,17 +52,19 @@ namespace ESM void StatState::save (ESMWriter &esm) const { esm.writeHNT ("STBA", mBase); - esm.writeHNT ("STMO", mMod); + + if (mMod != 0) + esm.writeHNT ("STMO", mMod); if (mCurrent) esm.writeHNT ("STCU", mCurrent); if (mDamage) - esm.writeHNT ("STDA", mDamage); + esm.writeHNT ("STDF", mDamage); if (mProgress) esm.writeHNT ("STPR", mProgress); } } -#endif \ No newline at end of file +#endif diff --git a/components/esm/variant.cpp b/components/esm/variant.cpp index a7859d128..4127a9d62 100644 --- a/components/esm/variant.cpp +++ b/components/esm/variant.cpp @@ -6,8 +6,41 @@ #include "esmreader.hpp" #include "variantimp.hpp" +#include "defs.hpp" + +namespace +{ + const uint32_t STRV = ESM::FourCC<'S','T','R','V'>::value; + const uint32_t INTV = ESM::FourCC<'I','N','T','V'>::value; + const uint32_t FLTV = ESM::FourCC<'F','L','T','V'>::value; +} + ESM::Variant::Variant() : mType (VT_None), mData (0) {} +ESM::Variant::Variant(const std::string &value) +{ + mData = 0; + mType = VT_None; + setType(VT_String); + setString(value); +} + +ESM::Variant::Variant(int value) +{ + mData = 0; + mType = VT_None; + setType(VT_Long); + setInteger(value); +} + +ESM::Variant::Variant(float value) +{ + mData = 0; + mType = VT_None; + setType(VT_Float); + setFloat(value); +} + ESM::Variant::~Variant() { delete mData; @@ -90,15 +123,17 @@ void ESM::Variant::read (ESMReader& esm, Format format) esm.getSubName(); NAME name = esm.retSubName(); - if (name=="STRV") + + + if (name==STRV) { type = VT_String; } - else if (name=="INTV") + else if (name==INTV) { type = VT_Int; } - else if (name=="FLTV") + else if (name==FLTV) { type = VT_Float; } @@ -111,11 +146,11 @@ void ESM::Variant::read (ESMReader& esm, Format format) esm.getSubName(); NAME name = esm.retSubName(); - if (name=="INTV") + if (name==INTV) { type = VT_Int; } - else if (name=="FLTV") + else if (name==FLTV) { type = VT_Float; } @@ -279,4 +314,4 @@ bool ESM::operator== (const Variant& left, const Variant& right) bool ESM::operator!= (const Variant& left, const Variant& right) { return !(left==right); -} \ No newline at end of file +} diff --git a/components/esm/variant.hpp b/components/esm/variant.hpp index 8ba9bb34f..d6c1a5489 100644 --- a/components/esm/variant.hpp +++ b/components/esm/variant.hpp @@ -38,6 +38,10 @@ namespace ESM Variant(); + Variant (const std::string& value); + Variant (int value); + Variant (float value); + ~Variant(); Variant& operator= (const Variant& variant); @@ -83,4 +87,4 @@ namespace ESM bool operator!= (const Variant& left, const Variant& right); } -#endif \ No newline at end of file +#endif diff --git a/components/esmterrain/storage.cpp b/components/esmterrain/storage.cpp new file mode 100644 index 000000000..3c76cc3b4 --- /dev/null +++ b/components/esmterrain/storage.cpp @@ -0,0 +1,530 @@ +#include "storage.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +namespace ESMTerrain +{ + + bool Storage::getMinMaxHeights(float size, const Ogre::Vector2 ¢er, float &min, float &max) + { + assert (size <= 1 && "Storage::getMinMaxHeights, chunk size should be <= 1 cell"); + + /// \todo investigate if min/max heights should be stored at load time in ESM::Land instead + + Ogre::Vector2 origin = center - Ogre::Vector2(size/2.f, size/2.f); + + assert(origin.x == (int) origin.x); + assert(origin.y == (int) origin.y); + + int cellX = origin.x; + int cellY = origin.y; + + const ESM::Land* land = getLand(cellX, cellY); + if (!land) + return false; + + min = std::numeric_limits::max(); + max = -std::numeric_limits::max(); + for (int row=0; rowmLandData->mHeights[col*ESM::Land::LAND_SIZE+row]; + if (h > max) + max = h; + if (h < min) + min = h; + } + } + return true; + } + + void Storage::fixNormal (Ogre::Vector3& normal, int cellX, int cellY, int col, int row) + { + while (col >= ESM::Land::LAND_SIZE-1) + { + ++cellY; + col -= ESM::Land::LAND_SIZE-1; + } + while (row >= ESM::Land::LAND_SIZE-1) + { + ++cellX; + row -= ESM::Land::LAND_SIZE-1; + } + while (col < 0) + { + --cellY; + col += ESM::Land::LAND_SIZE-1; + } + while (row < 0) + { + --cellX; + row += ESM::Land::LAND_SIZE-1; + } + ESM::Land* land = getLand(cellX, cellY); + if (land && land->mHasData) + { + normal.x = land->mLandData->mNormals[col*ESM::Land::LAND_SIZE*3+row*3]; + normal.y = land->mLandData->mNormals[col*ESM::Land::LAND_SIZE*3+row*3+1]; + normal.z = land->mLandData->mNormals[col*ESM::Land::LAND_SIZE*3+row*3+2]; + normal.normalise(); + } + else + normal = Ogre::Vector3(0,0,1); + } + + void Storage::averageNormal(Ogre::Vector3 &normal, int cellX, int cellY, int col, int row) + { + Ogre::Vector3 n1,n2,n3,n4; + fixNormal(n1, cellX, cellY, col+1, row); + fixNormal(n2, cellX, cellY, col-1, row); + fixNormal(n3, cellX, cellY, col, row+1); + fixNormal(n4, cellX, cellY, col, row-1); + normal = (n1+n2+n3+n4); + normal.normalise(); + } + + void Storage::fixColour (Ogre::ColourValue& color, int cellX, int cellY, int col, int row) + { + if (col == ESM::Land::LAND_SIZE-1) + { + ++cellY; + col = 0; + } + if (row == ESM::Land::LAND_SIZE-1) + { + ++cellX; + row = 0; + } + ESM::Land* land = getLand(cellX, cellY); + if (land && land->mLandData->mUsingColours) + { + color.r = land->mLandData->mColours[col*ESM::Land::LAND_SIZE*3+row*3] / 255.f; + color.g = land->mLandData->mColours[col*ESM::Land::LAND_SIZE*3+row*3+1] / 255.f; + color.b = land->mLandData->mColours[col*ESM::Land::LAND_SIZE*3+row*3+2] / 255.f; + } + else + { + color.r = 1; + color.g = 1; + color.b = 1; + } + + } + + void Storage::fillVertexBuffers (int lodLevel, float size, const Ogre::Vector2& center, Terrain::Alignment align, + std::vector& positions, + std::vector& normals, + std::vector& colours) + { + // LOD level n means every 2^n-th vertex is kept + size_t increment = 1 << lodLevel; + + Ogre::Vector2 origin = center - Ogre::Vector2(size/2.f, size/2.f); + assert(origin.x == (int) origin.x); + assert(origin.y == (int) origin.y); + + int startX = origin.x; + int startY = origin.y; + + size_t numVerts = size*(ESM::Land::LAND_SIZE-1)/increment + 1; + + colours.resize(numVerts*numVerts*4); + positions.resize(numVerts*numVerts*3); + normals.resize(numVerts*numVerts*3); + + Ogre::Vector3 normal; + Ogre::ColourValue color; + + float vertY = 0; + float vertX = 0; + + float vertY_ = 0; // of current cell corner + for (int cellY = startY; cellY < startY + std::ceil(size); ++cellY) + { + float vertX_ = 0; // of current cell corner + for (int cellX = startX; cellX < startX + std::ceil(size); ++cellX) + { + ESM::Land* land = getLand(cellX, cellY); + if (land && !land->mHasData) + land = NULL; + bool hasColors = land && land->mLandData->mUsingColours; + + int rowStart = 0; + int colStart = 0; + // Skip the first row / column unless we're at a chunk edge, + // since this row / column is already contained in a previous cell + if (colStart == 0 && vertY_ != 0) + colStart += increment; + if (rowStart == 0 && vertX_ != 0) + rowStart += increment; + + vertY = vertY_; + for (int col=colStart; colmLandData->mHeights[col*ESM::Land::LAND_SIZE+row]; + else + positions[vertX*numVerts*3 + vertY*3 + 2] = -2048; + + if (land) + { + normal.x = land->mLandData->mNormals[col*ESM::Land::LAND_SIZE*3+row*3]; + normal.y = land->mLandData->mNormals[col*ESM::Land::LAND_SIZE*3+row*3+1]; + normal.z = land->mLandData->mNormals[col*ESM::Land::LAND_SIZE*3+row*3+2]; + normal.normalise(); + } + else + normal = Ogre::Vector3(0,0,1); + + // Normals apparently don't connect seamlessly between cells + if (col == ESM::Land::LAND_SIZE-1 || row == ESM::Land::LAND_SIZE-1) + fixNormal(normal, cellX, cellY, col, row); + + // some corner normals appear to be complete garbage (z < 0) + if ((row == 0 || row == ESM::Land::LAND_SIZE-1) && (col == 0 || col == ESM::Land::LAND_SIZE-1)) + averageNormal(normal, cellX, cellY, col, row); + + assert(normal.z > 0); + + normals[vertX*numVerts*3 + vertY*3] = normal.x; + normals[vertX*numVerts*3 + vertY*3 + 1] = normal.y; + normals[vertX*numVerts*3 + vertY*3 + 2] = normal.z; + + if (hasColors) + { + color.r = land->mLandData->mColours[col*ESM::Land::LAND_SIZE*3+row*3] / 255.f; + color.g = land->mLandData->mColours[col*ESM::Land::LAND_SIZE*3+row*3+1] / 255.f; + color.b = land->mLandData->mColours[col*ESM::Land::LAND_SIZE*3+row*3+2] / 255.f; + } + else + { + color.r = 1; + color.g = 1; + color.b = 1; + } + + // Unlike normals, colors mostly connect seamlessly between cells, but not always... + if (col == ESM::Land::LAND_SIZE-1 || row == ESM::Land::LAND_SIZE-1) + fixColour(color, cellX, cellY, col, row); + + color.a = 1; + Ogre::uint32 rsColor; + Ogre::Root::getSingleton().getRenderSystem()->convertColourValue(color, &rsColor); + memcpy(&colours[vertX*numVerts*4 + vertY*4], &rsColor, sizeof(Ogre::uint32)); + + ++vertX; + } + ++vertY; + } + vertX_ = vertX; + } + vertY_ = vertY; + + assert(vertX_ == numVerts); // Ensure we covered whole area + } + assert(vertY_ == numVerts); // Ensure we covered whole area + } + + Storage::UniqueTextureId Storage::getVtexIndexAt(int cellX, int cellY, + int x, int y) + { + // For the first/last row/column, we need to get the texture from the neighbour cell + // to get consistent blending at the borders + --x; + if (x < 0) + { + --cellX; + x += ESM::Land::LAND_TEXTURE_SIZE; + } + if (y >= ESM::Land::LAND_TEXTURE_SIZE) // Y appears to be wrapped from the other side because why the hell not? + { + ++cellY; + y -= ESM::Land::LAND_TEXTURE_SIZE; + } + + assert(xmLandData->mTextures[y * ESM::Land::LAND_TEXTURE_SIZE + x]; + if (tex == 0) + return std::make_pair(0,0); // vtex 0 is always the base texture, regardless of plugin + return std::make_pair(tex, land->mPlugin); + } + else + return std::make_pair(0,0); + } + + std::string Storage::getTextureName(UniqueTextureId id) + { + if (id.first == 0) + return "textures\\_land_default.dds"; // Not sure if the default texture really is hardcoded? + + // NB: All vtex ids are +1 compared to the ltex ids + const ESM::LandTexture* ltex = getLandTexture(id.first-1, id.second); + + //TODO this is needed due to MWs messed up texture handling + std::string texture = Misc::ResourceHelpers::correctTexturePath(ltex->mTexture); + + return texture; + } + + void Storage::getBlendmaps (const std::vector& nodes, std::vector& out, bool pack) + { + for (std::vector::const_iterator it = nodes.begin(); it != nodes.end(); ++it) + { + out.push_back(Terrain::LayerCollection()); + out.back().mTarget = *it; + getBlendmapsImpl((*it)->getSize(), (*it)->getCenter(), pack, out.back().mBlendmaps, out.back().mLayers); + } + } + + void Storage::getBlendmaps(float chunkSize, const Ogre::Vector2 &chunkCenter, + bool pack, std::vector &blendmaps, std::vector &layerList) + { + getBlendmapsImpl(chunkSize, chunkCenter, pack, blendmaps, layerList); + } + + void Storage::getBlendmapsImpl(float chunkSize, const Ogre::Vector2 &chunkCenter, + bool pack, std::vector &blendmaps, std::vector &layerList) + { + // TODO - blending isn't completely right yet; the blending radius appears to be + // different at a cell transition (2 vertices, not 4), so we may need to create a larger blendmap + // and interpolate the rest of the cell by hand? :/ + + Ogre::Vector2 origin = chunkCenter - Ogre::Vector2(chunkSize/2.f, chunkSize/2.f); + int cellX = origin.x; + int cellY = origin.y; + + // Save the used texture indices so we know the total number of textures + // and number of required blend maps + std::set textureIndices; + // Due to the way the blending works, the base layer will always shine through in between + // blend transitions (eg halfway between two texels, both blend values will be 0.5, so 25% of base layer visible). + // To get a consistent look, we need to make sure to use the same base layer in all cells. + // So we're always adding _land_default.dds as the base layer here, even if it's not referenced in this cell. + textureIndices.insert(std::make_pair(0,0)); + + for (int y=0; y textureIndicesMap; + for (std::set::iterator it = textureIndices.begin(); it != textureIndices.end(); ++it) + { + int size = textureIndicesMap.size(); + textureIndicesMap[*it] = size; + layerList.push_back(getLayerInfo(getTextureName(*it))); + } + + int numTextures = textureIndices.size(); + // numTextures-1 since the base layer doesn't need blending + int numBlendmaps = pack ? std::ceil((numTextures-1) / 4.f) : (numTextures-1); + + int channels = pack ? 4 : 1; + + // Second iteration - create and fill in the blend maps + const int blendmapSize = ESM::Land::LAND_TEXTURE_SIZE+1; + + for (int i=0; isecond; + int blendIndex = (pack ? std::floor((layerIndex-1)/4.f) : layerIndex-1); + int channel = pack ? std::max(0, (layerIndex-1) % 4) : 0; + + if (blendIndex == i) + pData[y*blendmapSize*channels + x*channels + channel] = 255; + else + pData[y*blendmapSize*channels + x*channels + channel] = 0; + } + } + blendmaps.push_back(Ogre::PixelBox(blendmapSize, blendmapSize, 1, format, pData)); + } + } + + float Storage::getHeightAt(const Ogre::Vector3 &worldPos) + { + int cellX = std::floor(worldPos.x / 8192.f); + int cellY = std::floor(worldPos.y / 8192.f); + + ESM::Land* land = getLand(cellX, cellY); + if (!land) + return -2048; + + // Mostly lifted from Ogre::Terrain::getHeightAtTerrainPosition + + // Normalized position in the cell + float nX = (worldPos.x - (cellX * 8192))/8192.f; + float nY = (worldPos.y - (cellY * 8192))/8192.f; + + // get left / bottom points (rounded down) + float factor = ESM::Land::LAND_SIZE - 1.0f; + float invFactor = 1.0f / factor; + + int startX = static_cast(nX * factor); + int startY = static_cast(nY * factor); + int endX = startX + 1; + int endY = startY + 1; + + assert(endX < ESM::Land::LAND_SIZE); + assert(endY < ESM::Land::LAND_SIZE); + + // now get points in terrain space (effectively rounding them to boundaries) + float startXTS = startX * invFactor; + float startYTS = startY * invFactor; + float endXTS = endX * invFactor; + float endYTS = endY * invFactor; + + // get parametric from start coord to next point + float xParam = (nX - startXTS) * factor; + float yParam = (nY - startYTS) * factor; + + /* For even / odd tri strip rows, triangles are this shape: + even odd + 3---2 3---2 + | / | | \ | + 0---1 0---1 + */ + + // Build all 4 positions in normalized cell space, using point-sampled height + Ogre::Vector3 v0 (startXTS, startYTS, getVertexHeight(land, startX, startY) / 8192.f); + Ogre::Vector3 v1 (endXTS, startYTS, getVertexHeight(land, endX, startY) / 8192.f); + Ogre::Vector3 v2 (endXTS, endYTS, getVertexHeight(land, endX, endY) / 8192.f); + Ogre::Vector3 v3 (startXTS, endYTS, getVertexHeight(land, startX, endY) / 8192.f); + // define this plane in terrain space + Ogre::Plane plane; + // (At the moment, all rows have the same triangle alignment) + if (true) + { + // odd row + bool secondTri = ((1.0 - yParam) > xParam); + if (secondTri) + plane.redefine(v0, v1, v3); + else + plane.redefine(v1, v2, v3); + } + else + { + // even row + bool secondTri = (yParam > xParam); + if (secondTri) + plane.redefine(v0, v2, v3); + else + plane.redefine(v0, v1, v2); + } + + // Solve plane equation for z + return (-plane.normal.x * nX + -plane.normal.y * nY + - plane.d) / plane.normal.z * 8192; + + } + + float Storage::getVertexHeight(const ESM::Land *land, int x, int y) + { + assert(x < ESM::Land::LAND_SIZE); + assert(y < ESM::Land::LAND_SIZE); + return land->mLandData->mHeights[y * ESM::Land::LAND_SIZE + x]; + } + + Terrain::LayerInfo Storage::getLayerInfo(const std::string& texture) + { + // Already have this cached? + if (mLayerInfoMap.find(texture) != mLayerInfoMap.end()) + return mLayerInfoMap[texture]; + + Terrain::LayerInfo info; + info.mParallax = false; + info.mSpecular = false; + info.mDiffuseMap = texture; + std::string texture_ = texture; + boost::replace_last(texture_, ".", "_nh."); + + if (Ogre::ResourceGroupManager::getSingleton().resourceExistsInAnyGroup(texture_)) + { + info.mNormalMap = texture_; + info.mParallax = true; + } + else + { + texture_ = texture; + boost::replace_last(texture_, ".", "_n."); + if (Ogre::ResourceGroupManager::getSingleton().resourceExistsInAnyGroup(texture_)) + info.mNormalMap = texture_; + } + + texture_ = texture; + boost::replace_last(texture_, ".", "_diffusespec."); + if (Ogre::ResourceGroupManager::getSingleton().resourceExistsInAnyGroup(texture_)) + { + info.mDiffuseMap = texture_; + info.mSpecular = true; + } + + // This wasn't cached, so the textures are probably not loaded either. + // Background load them so they are hopefully already loaded once we need them! + Ogre::ResourceBackgroundQueue::getSingleton().load("Texture", info.mDiffuseMap, "General"); + if (!info.mNormalMap.empty()) + Ogre::ResourceBackgroundQueue::getSingleton().load("Texture", info.mNormalMap, "General"); + + mLayerInfoMap[texture] = info; + + return info; + } + + Terrain::LayerInfo Storage::getDefaultLayer() + { + Terrain::LayerInfo info; + info.mDiffuseMap = "textures\\_land_default.dds"; + info.mParallax = false; + info.mSpecular = false; + return info; + } + + float Storage::getCellWorldSize() + { + return ESM::Land::REAL_SIZE; + } + + int Storage::getCellVertices() + { + return ESM::Land::LAND_SIZE; + } + +} diff --git a/components/esmterrain/storage.hpp b/components/esmterrain/storage.hpp new file mode 100644 index 000000000..d25f7552b --- /dev/null +++ b/components/esmterrain/storage.hpp @@ -0,0 +1,117 @@ +#ifndef COMPONENTS_ESM_TERRAIN_STORAGE_H +#define COMPONENTS_ESM_TERRAIN_STORAGE_H + +#include + +#include +#include + +namespace ESMTerrain +{ + + /// @brief Feeds data from ESM terrain records (ESM::Land, ESM::LandTexture) + /// into the terrain component, converting it on the fly as needed. + class Storage : public Terrain::Storage + { + private: + + // Not implemented in this class, because we need different Store implementations for game and editor + virtual ESM::Land* getLand (int cellX, int cellY) = 0; + virtual const ESM::LandTexture* getLandTexture(int index, short plugin) = 0; + + public: + + // Not implemented in this class, because we need different Store implementations for game and editor + /// Get bounds of the whole terrain in cell units + virtual void getBounds(float& minX, float& maxX, float& minY, float& maxY) = 0; + + /// Get the minimum and maximum heights of a terrain region. + /// @note Will only be called for chunks with size = minBatchSize, i.e. leafs of the quad tree. + /// Larger chunks can simply merge AABB of children. + /// @param size size of the chunk in cell units + /// @param center center of the chunk in cell units + /// @param min min height will be stored here + /// @param max max height will be stored here + /// @return true if there was data available for this terrain chunk + virtual bool getMinMaxHeights (float size, const Ogre::Vector2& center, float& min, float& max); + + /// Fill vertex buffers for a terrain chunk. + /// @note May be called from background threads. Make sure to only call thread-safe functions from here! + /// @note returned colors need to be in render-system specific format! Use RenderSystem::convertColourValue. + /// @param lodLevel LOD level, 0 = most detailed + /// @param size size of the terrain chunk in cell units + /// @param center center of the chunk in cell units + /// @param positions buffer to write vertices + /// @param normals buffer to write vertex normals + /// @param colours buffer to write vertex colours + virtual void fillVertexBuffers (int lodLevel, float size, const Ogre::Vector2& center, Terrain::Alignment align, + std::vector& positions, + std::vector& normals, + std::vector& colours); + + /// Create textures holding layer blend values for a terrain chunk. + /// @note The terrain chunk shouldn't be larger than one cell since otherwise we might + /// have to do a ridiculous amount of different layers. For larger chunks, composite maps should be used. + /// @note May be called from background threads. + /// @param chunkSize size of the terrain chunk in cell units + /// @param chunkCenter center of the chunk in cell units + /// @param pack Whether to pack blend values for up to 4 layers into one texture (one in each channel) - + /// otherwise, each texture contains blend values for one layer only. Shader-based rendering + /// can utilize packing, FFP can't. + /// @param blendmaps created blendmaps will be written here + /// @param layerList names of the layer textures used will be written here + virtual void getBlendmaps (float chunkSize, const Ogre::Vector2& chunkCenter, bool pack, + std::vector& blendmaps, + std::vector& layerList); + + /// Retrieve pixel data for textures holding layer blend values for terrain chunks and layer texture information. + /// This variant is provided to eliminate the overhead of virtual function calls when retrieving a large number of blendmaps at once. + /// @note The terrain chunks shouldn't be larger than one cell since otherwise we might + /// have to do a ridiculous amount of different layers. For larger chunks, composite maps should be used. + /// @note May be called from background threads. + /// @param nodes A collection of nodes for which to retrieve the aforementioned data + /// @param out Output vector + /// @param pack Whether to pack blend values for up to 4 layers into one texture (one in each channel) - + /// otherwise, each texture contains blend values for one layer only. Shader-based rendering + /// can utilize packing, FFP can't. + virtual void getBlendmaps (const std::vector& nodes, std::vector& out, bool pack); + + virtual float getHeightAt (const Ogre::Vector3& worldPos); + + virtual Terrain::LayerInfo getDefaultLayer(); + + /// Get the transformation factor for mapping cell units to world units. + virtual float getCellWorldSize(); + + /// Get the number of vertices on one side for each cell. Should be (power of two)+1 + virtual int getCellVertices(); + + private: + void fixNormal (Ogre::Vector3& normal, int cellX, int cellY, int col, int row); + void fixColour (Ogre::ColourValue& colour, int cellX, int cellY, int col, int row); + void averageNormal (Ogre::Vector3& normal, int cellX, int cellY, int col, int row); + + float getVertexHeight (const ESM::Land* land, int x, int y); + + // Since plugins can define new texture palettes, we need to know the plugin index too + // in order to retrieve the correct texture name. + // pair + typedef std::pair UniqueTextureId; + + UniqueTextureId getVtexIndexAt(int cellX, int cellY, + int x, int y); + std::string getTextureName (UniqueTextureId id); + + std::map mLayerInfoMap; + + Terrain::LayerInfo getLayerInfo(const std::string& texture); + + // Non-virtual + void getBlendmapsImpl (float chunkSize, const Ogre::Vector2& chunkCenter, bool pack, + std::vector& blendmaps, + std::vector& layerList); + }; + +} + +#endif diff --git a/components/files/androidpath.cpp b/components/files/androidpath.cpp new file mode 100644 index 000000000..e2f9e948a --- /dev/null +++ b/components/files/androidpath.cpp @@ -0,0 +1,94 @@ +#include "androidpath.hpp" + +#if defined(__ANDROID__) + +#include +#include +#include +#include +#include + +namespace +{ + boost::filesystem::path getUserHome() + { + const char* dir = getenv("HOME"); + if (dir == NULL) + { + struct passwd* pwd = getpwuid(getuid()); + if (pwd != NULL) + { + dir = pwd->pw_dir; + } + } + if (dir == NULL) + return boost::filesystem::path(); + else + return boost::filesystem::path(dir); + } + + boost::filesystem::path getEnv(const std::string& envVariable, const boost::filesystem::path& fallback) + { + const char* result = getenv(envVariable.c_str()); + if (!result) + return fallback; + boost::filesystem::path dir(result); + if (dir.empty()) + return fallback; + else + return dir; + } +} + +/** + * \namespace Files + */ +namespace Files +{ + +AndroidPath::AndroidPath(const std::string& application_name) + : mName(application_name) +{ +} + +boost::filesystem::path AndroidPath::getUserConfigPath() const +{ + return getEnv("XDG_CONFIG_HOME", "/sdcard/libopenmw/config") / mName; +} + +boost::filesystem::path AndroidPath::getUserDataPath() const +{ + return getEnv("XDG_DATA_HOME", "/sdcard/libopenmw/share") / mName; +} + +boost::filesystem::path AndroidPath::getCachePath() const +{ + return getEnv("XDG_CACHE_HOME", "/sdcard/libopenmw/cache") / mName; +} + +boost::filesystem::path AndroidPath::getGlobalConfigPath() const +{ + boost::filesystem::path globalPath("/sdcard/libopenmw/"); + return globalPath / mName; +} + +boost::filesystem::path AndroidPath::getLocalPath() const +{ + return boost::filesystem::path("./"); +} + +boost::filesystem::path AndroidPath::getGlobalDataPath() const +{ + boost::filesystem::path globalDataPath("/sdcard/libopenmw/data"); + return globalDataPath / mName; +} + +boost::filesystem::path AndroidPath::getInstallPath() const +{ + return boost::filesystem::path(); +} + + +} /* namespace Files */ + +#endif /* defined(__Android__) */ diff --git a/components/files/androidpath.hpp b/components/files/androidpath.hpp new file mode 100644 index 000000000..792462fc6 --- /dev/null +++ b/components/files/androidpath.hpp @@ -0,0 +1,54 @@ +#ifndef COMPONENTS_FILES_ANDROIDPATH_H +#define COMPONENTS_FILES_ANDROIDPATH_H + +#if defined(__ANDROID__) + +#include +/** + * \namespace Files + */ +namespace Files +{ + +struct AndroidPath +{ + AndroidPath(const std::string& application_name); + + /** + * \brief Return path to the user directory. + */ + boost::filesystem::path getUserConfigPath() const; + + boost::filesystem::path getUserDataPath() const; + + /** + * \brief Return path to the global (system) directory where config files can be placed. + */ + boost::filesystem::path getGlobalConfigPath() const; + + /** + * \brief Return path to the runtime configuration directory which is the + * place where an application was started. + */ + boost::filesystem::path getLocalPath() const; + + /** + * \brief Return path to the global (system) directory where game files can be placed. + */ + boost::filesystem::path getGlobalDataPath() const; + + /** + * \brief + */ + boost::filesystem::path getCachePath() const; + + boost::filesystem::path getInstallPath() const; + + std::string mName; +}; + +} /* namespace Files */ + +#endif /* defined(__Android__) */ + +#endif /* COMPONENTS_FILES_ANDROIDPATH_H */ diff --git a/components/files/configurationmanager.cpp b/components/files/configurationmanager.cpp index ffa911b44..942f47d4e 100644 --- a/components/files/configurationmanager.cpp +++ b/components/files/configurationmanager.cpp @@ -16,13 +16,19 @@ namespace Files static const char* const openmwCfgFile = "openmw.cfg"; +#if defined(_WIN32) || defined(__WINDOWS__) +static const char* const applicationName = "OpenMW"; +#else +static const char* const applicationName = "openmw"; +#endif + const char* const mwToken = "?mw?"; const char* const localToken = "?local?"; -const char* const userToken = "?user?"; +const char* const userDataToken = "?userdata?"; const char* const globalToken = "?global?"; ConfigurationManager::ConfigurationManager() - : mFixedPath("openmw") + : mFixedPath(applicationName) { setupTokensMapping(); @@ -40,7 +46,7 @@ void ConfigurationManager::setupTokensMapping() { mTokensMapping.insert(std::make_pair(mwToken, &FixedPath<>::getInstallPath)); mTokensMapping.insert(std::make_pair(localToken, &FixedPath<>::getLocalPath)); - mTokensMapping.insert(std::make_pair(userToken, &FixedPath<>::getUserConfigPath)); + mTokensMapping.insert(std::make_pair(userDataToken, &FixedPath<>::getUserDataPath)); mTokensMapping.insert(std::make_pair(globalToken, &FixedPath<>::getGlobalDataPath)); } diff --git a/components/files/fixedpath.hpp b/components/files/fixedpath.hpp index cfd3458ce..9fb36d984 100644 --- a/components/files/fixedpath.hpp +++ b/components/files/fixedpath.hpp @@ -4,10 +4,14 @@ #include #include -#if defined(__linux__) || defined(__FreeBSD__) +#if defined(__linux__) || defined(__FreeBSD__) || defined(__FreeBSD_kernel__) +#ifndef ANDROID #include namespace Files { typedef LinuxPath TargetPathType; } - +#else + #include + namespace Files { typedef AndroidPath TargetPathType; } +#endif #elif defined(__WIN32) || defined(__WINDOWS__) || defined(_WIN32) #include namespace Files { typedef WindowsPath TargetPathType; } @@ -87,6 +91,7 @@ struct FixedPath return mLocalPath; } + const boost::filesystem::path& getInstallPath() const { return mInstallPath; diff --git a/components/files/linuxpath.cpp b/components/files/linuxpath.cpp index d285f4229..a105bb928 100644 --- a/components/files/linuxpath.cpp +++ b/components/files/linuxpath.cpp @@ -1,6 +1,6 @@ #include "linuxpath.hpp" -#if defined(__linux__) || defined(__FreeBSD__) +#if defined(__linux__) || defined(__FreeBSD__) || defined(__FreeBSD_kernel__) #include #include @@ -69,7 +69,7 @@ boost::filesystem::path LinuxPath::getCachePath() const boost::filesystem::path LinuxPath::getGlobalConfigPath() const { - boost::filesystem::path globalPath("/etc/"); + boost::filesystem::path globalPath(GLOBAL_CONFIG_PATH); return globalPath / mName; } @@ -80,7 +80,7 @@ boost::filesystem::path LinuxPath::getLocalPath() const boost::filesystem::path LinuxPath::getGlobalDataPath() const { - boost::filesystem::path globalDataPath("/usr/share/games/"); + boost::filesystem::path globalDataPath(GLOBAL_DATA_PATH); return globalDataPath / mName; } @@ -157,4 +157,4 @@ boost::filesystem::path LinuxPath::getInstallPath() const } /* namespace Files */ -#endif /* defined(__linux__) || defined(__FreeBSD__) */ +#endif /* defined(__linux__) || defined(__FreeBSD__) || defined(__FreeBSD_kernel__) */ diff --git a/components/files/linuxpath.hpp b/components/files/linuxpath.hpp index b710165b4..ba9756fc0 100644 --- a/components/files/linuxpath.hpp +++ b/components/files/linuxpath.hpp @@ -1,7 +1,7 @@ #ifndef COMPONENTS_FILES_LINUXPATH_H #define COMPONENTS_FILES_LINUXPATH_H -#if defined(__linux__) || defined(__FreeBSD__) +#if defined(__linux__) || defined(__FreeBSD__) || defined(__FreeBSD_kernel__) #include @@ -56,6 +56,6 @@ struct LinuxPath } /* namespace Files */ -#endif /* defined(__linux__) || defined(__FreeBSD__) */ +#endif /* defined(__linux__) || defined(__FreeBSD__) || defined(__FreeBSD_kernel__) */ #endif /* COMPONENTS_FILES_LINUXPATH_H */ diff --git a/apps/openmw/mwgui/fontloader.cpp b/components/fontloader/fontloader.cpp similarity index 64% rename from apps/openmw/mwgui/fontloader.cpp rename to components/fontloader/fontloader.cpp index 59c2e7ca6..e01e4b7bc 100644 --- a/apps/openmw/mwgui/fontloader.cpp +++ b/components/fontloader/fontloader.cpp @@ -64,10 +64,15 @@ namespace return unicode; } + // getUtf8, aka the worst function ever written. + // This includes various hacks for dealing with Morrowind's .fnt files that are *mostly* + // in the expected win12XX encoding, but also have randomly swapped characters sometimes. + // Looks like the Morrowind developers found standard encodings too boring and threw in some twists for fun. std::string getUtf8 (unsigned char c, ToUTF8::Utf8Encoder& encoder, ToUTF8::FromType encoding) { if (encoding == ToUTF8::WINDOWS_1250) { + // Hacks for polish font unsigned char win1250; std::map conv; conv[0x80] = 0xc6; @@ -101,7 +106,8 @@ namespace conv[0xa3] = 0xbf; conv[0xa4] = 0x0; // not contained in win1250 conv[0xe1] = 0x8c; - conv[0xe1] = 0x8c; + // Can't remember if this was supposed to read 0xe2, or is it just an extraneous copypaste? + //conv[0xe1] = 0x8c; conv[0xe3] = 0x0; // not contained in win1250 conv[0xf5] = 0x0; // not contained in win1250 @@ -117,7 +123,7 @@ namespace } -namespace MWGui +namespace Gui { FontLoader::FontLoader(ToUTF8::FromType encoding) @@ -128,7 +134,7 @@ namespace MWGui mEncoding = encoding; } - void FontLoader::loadAllFonts() + void FontLoader::loadAllFonts(bool exportToFile) { Ogre::StringVector groups = Ogre::ResourceGroupManager::getSingleton().getResourceGroups (); for (Ogre::StringVector::iterator it = groups.begin(); it != groups.end(); ++it) @@ -136,7 +142,7 @@ namespace MWGui Ogre::StringVectorPtr resourcesInThisGroup = Ogre::ResourceGroupManager::getSingleton ().findResourceNames (*it, "*.fnt"); for (Ogre::StringVector::iterator resource = resourcesInThisGroup->begin(); resource != resourcesInThisGroup->end(); ++resource) { - loadFont(*resource); + loadFont(*resource, exportToFile); } } } @@ -162,7 +168,7 @@ namespace MWGui float ascent; } GlyphInfo; - void FontLoader::loadFont(const std::string &fileName) + void FontLoader::loadFont(const std::string &fileName, bool exportToFile) { Ogre::DataStreamPtr file = Ogre::ResourceGroupManager::getSingleton().openResource(fileName); @@ -215,6 +221,9 @@ namespace MWGui width, height, 0, Ogre::PF_BYTE_RGBA); texture->loadImage(image); + if (exportToFile) + image.save(resourceName + ".png"); + // Register the font with MyGUI MyGUI::ResourceManualFont* font = static_cast( MyGUI::FactoryManager::getInstance().createObject("Resource", "ResourceManualFont")); @@ -234,10 +243,10 @@ namespace MWGui for(int i = 0; i < 256; i++) { - int x1 = data[i].top_left.x*width; - int y1 = data[i].top_left.y*height; - int w = data[i].top_right.x*width - x1; - int h = data[i].bottom_left.y*height - y1; + float x1 = data[i].top_left.x*width; + float y1 = data[i].top_left.y*height; + float w = data[i].top_right.x*width - x1; + float h = data[i].bottom_left.y*height - y1; ToUTF8::Utf8Encoder encoder(mEncoding); unsigned long unicodeVal = utf8ToUnicode(getUtf8(i, encoder, mEncoding)); @@ -251,6 +260,55 @@ namespace MWGui code->addAttribute("advance", data[i].width); code->addAttribute("bearing", MyGUI::utility::toString(data[i].kerning) + " " + MyGUI::utility::toString((fontSize-data[i].ascent))); + code->addAttribute("size", MyGUI::IntSize(data[i].width, data[i].height)); + + // More hacks! The french game uses several win1252 characters that are not included + // in the cp437 encoding of the font. Fall back to similar available characters. + if (mEncoding == ToUTF8::CP437) + { + std::multimap additional; // + additional.insert(std::make_pair(39, 0x2019)); // apostrophe + additional.insert(std::make_pair(45, 0x2013)); // dash + additional.insert(std::make_pair(45, 0x2014)); // dash + additional.insert(std::make_pair(34, 0x201D)); // right double quotation mark + additional.insert(std::make_pair(34, 0x201C)); // left double quotation mark + additional.insert(std::make_pair(44, 0x201A)); + additional.insert(std::make_pair(44, 0x201E)); + additional.insert(std::make_pair(43, 0x2020)); + additional.insert(std::make_pair(94, 0x02C6)); + additional.insert(std::make_pair(37, 0x2030)); + additional.insert(std::make_pair(83, 0x0160)); + additional.insert(std::make_pair(60, 0x2039)); + additional.insert(std::make_pair(79, 0x0152)); + additional.insert(std::make_pair(90, 0x017D)); + additional.insert(std::make_pair(39, 0x2019)); + additional.insert(std::make_pair(126, 0x02DC)); + additional.insert(std::make_pair(84, 0x2122)); + additional.insert(std::make_pair(83, 0x0161)); + additional.insert(std::make_pair(62, 0x203A)); + additional.insert(std::make_pair(111, 0x0153)); + additional.insert(std::make_pair(122, 0x017E)); + additional.insert(std::make_pair(89, 0x0178)); + additional.insert(std::make_pair(156, 0x00A2)); + additional.insert(std::make_pair(46, 0x2026)); + + for (std::multimap::iterator it = additional.begin(); it != additional.end(); ++it) + { + if (it->first != i) + continue; + + MyGUI::xml::ElementPtr code = codes->createChild("Code"); + code->addAttribute("index", it->second); + code->addAttribute("coord", MyGUI::utility::toString(x1) + " " + + MyGUI::utility::toString(y1) + " " + + MyGUI::utility::toString(w) + " " + + MyGUI::utility::toString(h)); + code->addAttribute("advance", data[i].width); + code->addAttribute("bearing", MyGUI::utility::toString(data[i].kerning) + " " + + MyGUI::utility::toString((fontSize-data[i].ascent))); + code->addAttribute("size", MyGUI::IntSize(data[i].width, data[i].height)); + } + } // ASCII vertical bar, use this as text input cursor if (i == 124) @@ -264,26 +322,46 @@ namespace MWGui cursorCode->addAttribute("advance", data[i].width); cursorCode->addAttribute("bearing", MyGUI::utility::toString(data[i].kerning) + " " + MyGUI::utility::toString((fontSize-data[i].ascent))); + cursorCode->addAttribute("size", MyGUI::IntSize(data[i].width, data[i].height)); + } + + // Question mark, use for NotDefined marker (used for glyphs not existing in the font) + if (i == 63) + { + MyGUI::xml::ElementPtr cursorCode = codes->createChild("Code"); + cursorCode->addAttribute("index", MyGUI::FontCodeType::NotDefined); + cursorCode->addAttribute("coord", MyGUI::utility::toString(x1) + " " + + MyGUI::utility::toString(y1) + " " + + MyGUI::utility::toString(w) + " " + + MyGUI::utility::toString(h)); + cursorCode->addAttribute("advance", data[i].width); + cursorCode->addAttribute("bearing", MyGUI::utility::toString(data[i].kerning) + " " + + MyGUI::utility::toString((fontSize-data[i].ascent))); + cursorCode->addAttribute("size", MyGUI::IntSize(data[i].width, data[i].height)); } } // These are required as well, but the fonts don't provide them - for (int i=0; i<3; ++i) + for (int i=0; i<2; ++i) { MyGUI::FontCodeType::Enum type; if(i == 0) type = MyGUI::FontCodeType::Selected; else if (i == 1) type = MyGUI::FontCodeType::SelectedBack; - else if (i == 2) - type = MyGUI::FontCodeType::NotDefined; MyGUI::xml::ElementPtr cursorCode = codes->createChild("Code"); cursorCode->addAttribute("index", type); cursorCode->addAttribute("coord", "0 0 0 0"); cursorCode->addAttribute("advance", "0"); cursorCode->addAttribute("bearing", "0 0"); + cursorCode->addAttribute("size", "0 0"); + } + if (exportToFile) + { + xmlDocument.createDeclaration(); + xmlDocument.save(resourceName + ".xml"); } font->deserialization(root, MyGUI::Version(3,2,0)); diff --git a/components/fontloader/fontloader.hpp b/components/fontloader/fontloader.hpp new file mode 100644 index 000000000..a41506dbb --- /dev/null +++ b/components/fontloader/fontloader.hpp @@ -0,0 +1,28 @@ +#ifndef MWGUI_FONTLOADER_H +#define MWGUI_FONTLOADER_H + +#include + +namespace Gui +{ + + + /// @brief loads Morrowind's .fnt/.tex fonts for use with MyGUI and Ogre + class FontLoader + { + public: + FontLoader (ToUTF8::FromType encoding); + + /// @param exportToFile export the converted fonts (Images and XML with glyph metrics) to files? + void loadAllFonts (bool exportToFile); + + private: + ToUTF8::FromType mEncoding; + + /// @param exportToFile export the converted font (Image and XML with glyph metrics) to files? + void loadFont (const std::string& fileName, bool exportToFile); + }; + +} + +#endif diff --git a/components/interpreter/context.hpp b/components/interpreter/context.hpp index 97e4fad4f..881687366 100644 --- a/components/interpreter/context.hpp +++ b/components/interpreter/context.hpp @@ -81,7 +81,7 @@ namespace Interpreter virtual bool isScriptRunning (const std::string& name) const = 0; - virtual void startScript (const std::string& name) = 0; + virtual void startScript (const std::string& name, const std::string& targetId = "") = 0; virtual void stopScript (const std::string& name) = 0; @@ -108,6 +108,8 @@ namespace Interpreter virtual void setMemberFloat (const std::string& id, const std::string& name, float value, bool global) = 0; + + virtual std::string getTargetId() const = 0; }; } diff --git a/components/interpreter/defines.cpp b/components/interpreter/defines.cpp index 6f3b5bb5a..94916ee85 100644 --- a/components/interpreter/defines.cpp +++ b/components/interpreter/defines.cpp @@ -4,10 +4,12 @@ #include #include #include +#include namespace Interpreter{ - bool Check(const std::string& str, const std::string& escword, unsigned int* i, unsigned int* start){ + bool check(const std::string& str, const std::string& escword, unsigned int* i, unsigned int* start) + { bool retval = str.find(escword) == 0; if(retval){ (*i) += escword.length(); @@ -18,170 +20,181 @@ namespace Interpreter{ std::vector globals; - bool longerStr(const std::string& a, const std::string& b){ + bool longerStr(const std::string& a, const std::string& b) + { return a.length() > b.length(); } - std::string fixDefinesReal(std::string text, char eschar, bool isBook, Context& context){ + std::string fixDefinesReal(std::string text, char eschar, bool isBook, Context& context) + { unsigned int start = 0; std::ostringstream retval; - for(unsigned int i = 0; i < text.length(); i++){ - if(text[i] == eschar){ + for(unsigned int i = 0; i < text.length(); i++) + { + if(text[i] == eschar) + { retval << text.substr(start, i - start); std::string temp = text.substr(i+1, 100); transform(temp.begin(), temp.end(), temp.begin(), ::tolower); - bool found; - - if( (found = Check(temp, "actionslideright", &i, &start))){ - retval << context.getActionBinding("#{sRight}"); - } - else if((found = Check(temp, "actionreadymagic", &i, &start))){ - retval << context.getActionBinding("#{sReady_Magic}"); - } - else if((found = Check(temp, "actionprevweapon", &i, &start))){ - retval << "PLACEHOLDER_ACTION_PREV_WEAPON"; - } - else if((found = Check(temp, "actionnextweapon", &i, &start))){ - retval << "PLACEHOLDER_ACTION_PREV_WEAPON"; - } - else if((found = Check(temp, "actiontogglerun", &i, &start))){ - retval << context.getActionBinding("#{sAuto_Run}"); - } - else if((found = Check(temp, "actionslideleft", &i, &start))){ - retval << context.getActionBinding("#{sLeft}"); - } - else if((found = Check(temp, "actionreadyitem", &i, &start))){ - retval << context.getActionBinding("#{sReady_Weapon}"); - } - else if((found = Check(temp, "actionprevspell", &i, &start))){ - retval << "PLACEHOLDER_ACTION_PREV_SPELL"; - } - else if((found = Check(temp, "actionnextspell", &i, &start))){ - retval << "PLACEHOLDER_ACTION_NEXT_SPELL"; - } - else if((found = Check(temp, "actionrestmenu", &i, &start))){ - retval << context.getActionBinding("#{sRestKey}"); - } - else if((found = Check(temp, "actionmenumode", &i, &start))){ - retval << context.getActionBinding("#{sInventory}"); - } - else if((found = Check(temp, "actionactivate", &i, &start))){ - retval << context.getActionBinding("#{sActivate}"); - } - else if((found = Check(temp, "actionjournal", &i, &start))){ - retval << context.getActionBinding("#{sJournal}"); - } - else if((found = Check(temp, "actionforward", &i, &start))){ - retval << context.getActionBinding("#{sForward}"); - } - else if((found = Check(temp, "pccrimelevel", &i, &start))){ - retval << context.getPCBounty(); - } - else if((found = Check(temp, "actioncrouch", &i, &start))){ - retval << context.getActionBinding("#{sCrouch_Sneak}"); - } - else if((found = Check(temp, "actionjump", &i, &start))){ - retval << context.getActionBinding("#{sJump}"); - } - else if((found = Check(temp, "actionback", &i, &start))){ - retval << context.getActionBinding("#{sBack}"); - } - else if((found = Check(temp, "actionuse", &i, &start))){ - retval << context.getActionBinding("#{sUse}"); - } - else if((found = Check(temp, "actionrun", &i, &start))){ - retval << context.getActionBinding("#{sRun}"); - } - else if((found = Check(temp, "pcclass", &i, &start))){ - retval << context.getPCClass(); - } - else if((found = Check(temp, "pcrace", &i, &start))){ - retval << context.getPCRace(); - } - else if((found = Check(temp, "pcname", &i, &start))){ - retval << context.getPCName(); - } - else if((found = Check(temp, "cell", &i, &start))){ - retval << context.getCurrentCellName(); - } - - else if(eschar == '%' && !isBook) { // In Dialogue, not messagebox - if( (found = Check(temp, "faction", &i, &start))){ - retval << context.getNPCFaction(); + bool found = false; + try + { + if( (found = check(temp, "actionslideright", &i, &start))){ + retval << context.getActionBinding("#{sRight}"); } - else if((found = Check(temp, "nextpcrank", &i, &start))){ - retval << context.getPCNextRank(); + else if((found = check(temp, "actionreadymagic", &i, &start))){ + retval << context.getActionBinding("#{sReady_Magic}"); } - else if((found = Check(temp, "pcnextrank", &i, &start))){ - retval << context.getPCNextRank(); + else if((found = check(temp, "actionprevweapon", &i, &start))){ + retval << "PLACEHOLDER_ACTION_PREV_WEAPON"; } - else if((found = Check(temp, "pcrank", &i, &start))){ - retval << context.getPCRank(); + else if((found = check(temp, "actionnextweapon", &i, &start))){ + retval << "PLACEHOLDER_ACTION_PREV_WEAPON"; } - else if((found = Check(temp, "rank", &i, &start))){ - retval << context.getNPCRank(); + else if((found = check(temp, "actiontogglerun", &i, &start))){ + retval << context.getActionBinding("#{sAuto_Run}"); } - - else if((found = Check(temp, "class", &i, &start))){ - retval << context.getNPCClass(); + else if((found = check(temp, "actionslideleft", &i, &start))){ + retval << context.getActionBinding("#{sLeft}"); } - else if((found = Check(temp, "race", &i, &start))){ - retval << context.getNPCRace(); + else if((found = check(temp, "actionreadyitem", &i, &start))){ + retval << context.getActionBinding("#{sReady_Weapon}"); } - else if((found = Check(temp, "name", &i, &start))){ - retval << context.getNPCName(); + else if((found = check(temp, "actionprevspell", &i, &start))){ + retval << "PLACEHOLDER_ACTION_PREV_SPELL"; } - } - else { // In messagebox or book, not dialogue - - /* empty outside dialogue */ - if( (found = Check(temp, "faction", &i, &start))); - else if((found = Check(temp, "nextpcrank", &i, &start))); - else if((found = Check(temp, "pcnextrank", &i, &start))); - else if((found = Check(temp, "pcrank", &i, &start))); - else if((found = Check(temp, "rank", &i, &start))); - - /* uses pc in messageboxes */ - else if((found = Check(temp, "class", &i, &start))){ + else if((found = check(temp, "actionnextspell", &i, &start))){ + retval << "PLACEHOLDER_ACTION_NEXT_SPELL"; + } + else if((found = check(temp, "actionrestmenu", &i, &start))){ + retval << context.getActionBinding("#{sRestKey}"); + } + else if((found = check(temp, "actionmenumode", &i, &start))){ + retval << context.getActionBinding("#{sInventory}"); + } + else if((found = check(temp, "actionactivate", &i, &start))){ + retval << context.getActionBinding("#{sActivate}"); + } + else if((found = check(temp, "actionjournal", &i, &start))){ + retval << context.getActionBinding("#{sJournal}"); + } + else if((found = check(temp, "actionforward", &i, &start))){ + retval << context.getActionBinding("#{sForward}"); + } + else if((found = check(temp, "pccrimelevel", &i, &start))){ + retval << context.getPCBounty(); + } + else if((found = check(temp, "actioncrouch", &i, &start))){ + retval << context.getActionBinding("#{sCrouch_Sneak}"); + } + else if((found = check(temp, "actionjump", &i, &start))){ + retval << context.getActionBinding("#{sJump}"); + } + else if((found = check(temp, "actionback", &i, &start))){ + retval << context.getActionBinding("#{sBack}"); + } + else if((found = check(temp, "actionuse", &i, &start))){ + retval << context.getActionBinding("#{sUse}"); + } + else if((found = check(temp, "actionrun", &i, &start))){ + retval << context.getActionBinding("#{sRun}"); + } + else if((found = check(temp, "pcclass", &i, &start))){ retval << context.getPCClass(); } - else if((found = Check(temp, "race", &i, &start))){ + else if((found = check(temp, "pcrace", &i, &start))){ retval << context.getPCRace(); } - else if((found = Check(temp, "name", &i, &start))){ + else if((found = check(temp, "pcname", &i, &start))){ retval << context.getPCName(); } - } - - /* Not a builtin, try global variables */ - if(!found){ - /* if list of globals is empty, grab it and sort it by descending string length */ - if(globals.empty()){ - globals = context.getGlobals(); - sort(globals.begin(), globals.end(), longerStr); + else if((found = check(temp, "cell", &i, &start))){ + retval << context.getCurrentCellName(); } - for(unsigned int j = 0; j < globals.size(); j++){ - if(globals[j].length() > temp.length()){ // Just in case there's a global with a huuuge name - std::string temp = text.substr(i+1, globals[j].length()); - transform(temp.begin(), temp.end(), temp.begin(), ::tolower); + else if(eschar == '%' && !isBook) { // In Dialogue, not messagebox + if( (found = check(temp, "faction", &i, &start))){ + retval << context.getNPCFaction(); + } + else if((found = check(temp, "nextpcrank", &i, &start))){ + retval << context.getPCNextRank(); + } + else if((found = check(temp, "pcnextrank", &i, &start))){ + retval << context.getPCNextRank(); + } + else if((found = check(temp, "pcrank", &i, &start))){ + retval << context.getPCRank(); + } + else if((found = check(temp, "rank", &i, &start))){ + retval << context.getNPCRank(); } - if((found = Check(temp, globals[j], &i, &start))){ - char type = context.getGlobalType(globals[j]); + else if((found = check(temp, "class", &i, &start))){ + retval << context.getNPCClass(); + } + else if((found = check(temp, "race", &i, &start))){ + retval << context.getNPCRace(); + } + else if((found = check(temp, "name", &i, &start))){ + retval << context.getNPCName(); + } + } + else { // In messagebox or book, not dialogue + + /* empty outside dialogue */ + if( (found = check(temp, "faction", &i, &start))); + else if((found = check(temp, "nextpcrank", &i, &start))); + else if((found = check(temp, "pcnextrank", &i, &start))); + else if((found = check(temp, "pcrank", &i, &start))); + else if((found = check(temp, "rank", &i, &start))); + + /* uses pc in messageboxes */ + else if((found = check(temp, "class", &i, &start))){ + retval << context.getPCClass(); + } + else if((found = check(temp, "race", &i, &start))){ + retval << context.getPCRace(); + } + else if((found = check(temp, "name", &i, &start))){ + retval << context.getPCName(); + } + } - switch(type){ - case 's': retval << context.getGlobalShort(globals[j]); break; - case 'l': retval << context.getGlobalLong(globals[j]); break; - case 'f': retval << context.getGlobalFloat(globals[j]); break; + /* Not a builtin, try global variables */ + if(!found){ + /* if list of globals is empty, grab it and sort it by descending string length */ + if(globals.empty()){ + globals = context.getGlobals(); + sort(globals.begin(), globals.end(), longerStr); + } + + for(unsigned int j = 0; j < globals.size(); j++){ + if(globals[j].length() > temp.length()){ // Just in case there's a global with a huuuge name + std::string temp = text.substr(i+1, globals[j].length()); + transform(temp.begin(), temp.end(), temp.begin(), ::tolower); + } + + if((found = check(temp, globals[j], &i, &start))){ + char type = context.getGlobalType(globals[j]); + + switch(type){ + case 's': retval << context.getGlobalShort(globals[j]); break; + case 'l': retval << context.getGlobalLong(globals[j]); break; + case 'f': retval << context.getGlobalFloat(globals[j]); break; + } + break; } - break; } } } - - /* Not found */ + catch (std::exception& e) + { + std::cerr << "Failed to replace escape character, with the following error: " << e.what() << std::endl; + std::cerr << "Full text below: " << std::endl << text << std::endl; + } + + // Not found, or error if(!found){ /* leave unmodified */ i += 1; diff --git a/components/interpreter/docs/vmformat.txt b/components/interpreter/docs/vmformat.txt index 990762268..5d1eba088 100644 --- a/components/interpreter/docs/vmformat.txt +++ b/components/interpreter/docs/vmformat.txt @@ -133,5 +133,6 @@ op 67: store stack[0] in member float stack[2] of global script with ID stack[1] op 68: replace stack[0] with member short stack[1] of global script with ID stack[0] op 69: replace stack[0] with member short stack[1] of global script with ID stack[0] op 70: replace stack[0] with member short stack[1] of global script with ID stack[0] -opcodes 71-33554431 unused +op 71: explicit reference (target) = stack[0]; pop; start script stack[0] and pop +opcodes 72-33554431 unused opcodes 33554432-67108863 reserved for extensions diff --git a/components/interpreter/installopcodes.cpp b/components/interpreter/installopcodes.cpp index 721cde3d8..d705a109c 100644 --- a/components/interpreter/installopcodes.cpp +++ b/components/interpreter/installopcodes.cpp @@ -113,6 +113,7 @@ namespace Interpreter interpreter.installSegment5 (46, new OpScriptRunning); interpreter.installSegment5 (47, new OpStartScript); interpreter.installSegment5 (48, new OpStopScript); + interpreter.installSegment5 (71, new OpStartScriptExplicit); // spacial interpreter.installSegment5 (49, new OpGetDistance); diff --git a/components/interpreter/scriptopcodes.hpp b/components/interpreter/scriptopcodes.hpp index 56502d510..976390eb5 100644 --- a/components/interpreter/scriptopcodes.hpp +++ b/components/interpreter/scriptopcodes.hpp @@ -10,36 +10,52 @@ namespace Interpreter class OpScriptRunning : public Opcode0 { public: - + virtual void execute (Runtime& runtime) { std::string name = runtime.getStringLiteral (runtime[0].mInteger); runtime[0].mInteger = runtime.getContext().isScriptRunning (name); - } + } }; class OpStartScript : public Opcode0 { public: - + virtual void execute (Runtime& runtime) { std::string name = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); - runtime.getContext().startScript (name); - } + runtime.getContext().startScript (name, runtime.getContext().getTargetId()); + } + }; + + class OpStartScriptExplicit : public Opcode0 + { + public: + + virtual void execute (Runtime& runtime) + { + std::string targetId = runtime.getStringLiteral (runtime[0].mInteger); + runtime.pop(); + + std::string name = runtime.getStringLiteral (runtime[0].mInteger); + runtime.pop(); + + runtime.getContext().startScript (name, targetId); + } }; class OpStopScript : public Opcode0 { public: - + virtual void execute (Runtime& runtime) { std::string name = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); runtime.getContext().stopScript (name); - } + } }; } diff --git a/components/loadinglistener/loadinglistener.hpp b/components/loadinglistener/loadinglistener.hpp index f6a7b71e9..47962015c 100644 --- a/components/loadinglistener/loadinglistener.hpp +++ b/components/loadinglistener/loadinglistener.hpp @@ -18,9 +18,6 @@ namespace Loading virtual void setProgressRange (size_t range) = 0; virtual void setProgress (size_t value) = 0; virtual void increaseProgress (size_t increase = 1) = 0; - - /// Indicate the scene is now ready to be shown - virtual void removeWallpaper() = 0; }; // Used for stopping a loading sequence when the object goes out of scope diff --git a/components/misc/resourcehelpers.cpp b/components/misc/resourcehelpers.cpp new file mode 100644 index 000000000..9eaf441ef --- /dev/null +++ b/components/misc/resourcehelpers.cpp @@ -0,0 +1,90 @@ +#include "resourcehelpers.hpp" + +#include + +#include + +bool Misc::ResourceHelpers::changeExtensionToDds(std::string &path) +{ + Ogre::String::size_type pos = path.rfind('.'); + if(pos != Ogre::String::npos && path.compare(pos, path.length() - pos, ".dds") != 0) + { + path.replace(pos, path.length(), ".dds"); + return true; + } + return false; +} + +std::string Misc::ResourceHelpers::correctResourcePath(const std::string &topLevelDirectory, const std::string &resPath) +{ + /* Bethesda at some point converted all their BSA + * textures from tga to dds for increased load speed, but all + * texture file name references were kept as .tga. + */ + + std::string prefix1 = topLevelDirectory + '\\'; + std::string prefix2 = topLevelDirectory + '/'; + + std::string correctedPath = resPath; + Misc::StringUtils::toLower(correctedPath); + + // Apparently, leading separators are allowed + while (correctedPath.size() && (correctedPath[0] == '/' || correctedPath[0] == '\\')) + correctedPath.erase(0, 1); + + if(correctedPath.compare(0, prefix1.size(), prefix1.data()) != 0 && + correctedPath.compare(0, prefix2.size(), prefix2.data()) != 0) + correctedPath = prefix1 + correctedPath; + + std::string origExt = correctedPath; + + // since we know all (GOTY edition or less) textures end + // in .dds, we change the extension + if (changeExtensionToDds(correctedPath)) + { + // if it turns out that the above wasn't true in all cases (not for vanilla, but maybe mods) + // verify, and revert if false (this call succeeds quickly, but fails slowly) + if(!Ogre::ResourceGroupManager::getSingleton().resourceExistsInAnyGroup(correctedPath)) + { + return origExt; + } + } + + return correctedPath; +} + +std::string Misc::ResourceHelpers::correctTexturePath(const std::string &resPath) +{ + static const std::string dir = "textures"; + return correctResourcePath(dir, resPath); +} + +std::string Misc::ResourceHelpers::correctIconPath(const std::string &resPath) +{ + static const std::string dir = "icons"; + return correctResourcePath(dir, resPath); +} + +std::string Misc::ResourceHelpers::correctBookartPath(const std::string &resPath) +{ + static const std::string dir = "bookart"; + std::string image = correctResourcePath(dir, resPath); + + return image; +} + +std::string Misc::ResourceHelpers::correctBookartPath(const std::string &resPath, int width, int height) +{ + std::string image = correctBookartPath(resPath); + + // Apparently a bug with some morrowind versions, they reference the image without the size suffix. + // So if the image isn't found, try appending the size. + if (!Ogre::ResourceGroupManager::getSingleton().resourceExistsInAnyGroup(image)) + { + std::stringstream str; + str << image.substr(0, image.rfind('.')) << "_" << width << "_" << height << image.substr(image.rfind('.')); + image = Misc::ResourceHelpers::correctBookartPath(str.str()); + } + + return image; +} diff --git a/components/misc/resourcehelpers.hpp b/components/misc/resourcehelpers.hpp new file mode 100644 index 000000000..3cf0f4c27 --- /dev/null +++ b/components/misc/resourcehelpers.hpp @@ -0,0 +1,19 @@ +#ifndef MISC_RESOURCEHELPERS_H +#define MISC_RESOURCEHELPERS_H + +#include + +namespace Misc +{ + namespace ResourceHelpers + { + bool changeExtensionToDds(std::string &path); + std::string correctResourcePath(const std::string &topLevelDirectory, const std::string &resPath); + std::string correctTexturePath(const std::string &resPath); + std::string correctIconPath(const std::string &resPath); + std::string correctBookartPath(const std::string &resPath); + std::string correctBookartPath(const std::string &resPath, int width, int height); + } +} + +#endif diff --git a/components/misc/stringops.cpp b/components/misc/stringops.cpp index 0bc8e290a..0f801e554 100644 --- a/components/misc/stringops.cpp +++ b/components/misc/stringops.cpp @@ -12,59 +12,6 @@ namespace Misc { -bool begins(const char* str1, const char* str2) -{ - while(*str2) - { - if(*str1 == 0 || *str1 != *str2) return false; - - str1++; - str2++; - } - return true; -} - -bool ends(const char* str1, const char* str2) -{ - int len1 = strlen(str1); - int len2 = strlen(str2); - - if(len1 < len2) return false; - - return strcmp(str2, str1+len1-len2) == 0; -} - -// True if the given chars match, case insensitive -static bool icmp(char a, char b) -{ - if(a >= 'A' && a <= 'Z') - a += 'a' - 'A'; - if(b >= 'A' && b <= 'Z') - b += 'a' - 'A'; - - return a == b; -} - -bool ibegins(const char* str1, const char* str2) -{ - while(*str2) - { - if(*str1 == 0 || !icmp(*str1,*str2)) return false; - - str1++; - str2++; - } - return true; -} - -bool iends(const char* str1, const char* str2) -{ - int len1 = strlen(str1); - int len2 = strlen(str2); - - if(len1 < len2) return false; - - return strcasecmp(str2, str1+len1-len2) == 0; -} +std::locale StringUtils::mLocale = std::locale::classic(); } diff --git a/components/misc/stringops.hpp b/components/misc/stringops.hpp index d41463cfc..04dedb072 100644 --- a/components/misc/stringops.hpp +++ b/components/misc/stringops.hpp @@ -4,15 +4,18 @@ #include #include #include +#include namespace Misc { class StringUtils { + + static std::locale mLocale; struct ci { - bool operator()(int x, int y) const { - return std::tolower(x) < std::tolower(y); + bool operator()(char x, char y) const { + return std::tolower(x, StringUtils::mLocale) < std::tolower(y, StringUtils::mLocale); } }; @@ -28,7 +31,7 @@ public: std::string::const_iterator xit = x.begin(); std::string::const_iterator yit = y.begin(); for (; xit != x.end(); ++xit, ++yit) { - if (std::tolower(*xit) != std::tolower(*yit)) { + if (std::tolower(*xit, mLocale) != std::tolower(*yit, mLocale)) { return false; } } @@ -42,7 +45,7 @@ public: for(;xit != x.end() && yit != y.end() && len > 0;++xit,++yit,--len) { int res = *xit - *yit; - if(res != 0 && std::tolower(*xit) != std::tolower(*yit)) + if(res != 0 && std::tolower(*xit, mLocale) != std::tolower(*yit, mLocale)) return (res > 0) ? 1 : -1; } if(len > 0) @@ -57,12 +60,8 @@ public: /// Transforms input string to lower case w/o copy static std::string &toLower(std::string &inout) { - std::transform( - inout.begin(), - inout.end(), - inout.begin(), - (int (*)(int)) std::tolower - ); + for (unsigned int i=0; igetUShort(); + + frequency = nif->getFloat(); + phase = nif->getFloat(); + timeStart = nif->getFloat(); + timeStop = nif->getFloat(); + + target.read(nif); + } + + void post(NIFFile *nif) + { + Record::post(nif); + next.post(nif); + target.post(nif); + } +}; + +/// Anything that has a controller +class Controlled : public Extra +{ +public: + ControllerPtr controller; + + void read(NIFStream *nif) + { + Extra::read(nif); + controller.read(nif); + } + + void post(NIFFile *nif) + { + Extra::post(nif); + controller.post(nif); + } +}; + +/// Has name, extra-data and controller +class Named : public Controlled +{ +public: + std::string name; + + void read(NIFStream *nif) + { + name = nif->getString(); + Controlled::read(nif); + } +}; +typedef Named NiSequenceStreamHelper; + +} // Namespace +#endif diff --git a/components/nif/controlled.hpp b/components/nif/controlled.hpp index 6acb8ff20..815aa7d3f 100644 --- a/components/nif/controlled.hpp +++ b/components/nif/controlled.hpp @@ -24,44 +24,70 @@ #ifndef OPENMW_COMPONENTS_NIF_CONTROLLED_HPP #define OPENMW_COMPONENTS_NIF_CONTROLLED_HPP -#include "extra.hpp" -#include "controller.hpp" +#include "base.hpp" namespace Nif { -/// Anything that has a controller -class Controlled : public Extra +class NiSourceTexture : public Named { public: - ControllerPtr controller; + // Is this an external (references a separate texture file) or + // internal (data is inside the nif itself) texture? + bool external; + + std::string filename; // In case of external textures + NiPixelDataPtr data; // In case of internal textures + + /* Pixel layout + 0 - Palettised + 1 - High color 16 + 2 - True color 32 + 3 - Compressed + 4 - Bumpmap + 5 - Default */ + int pixel; + + /* Mipmap format + 0 - no + 1 - yes + 2 - default */ + int mipmap; + + /* Alpha + 0 - none + 1 - binary + 2 - smooth + 3 - default (use material alpha, or multiply material with texture if present) + */ + int alpha; void read(NIFStream *nif) { - Extra::read(nif); - controller.read(nif); + Named::read(nif); + + external = !!nif->getChar(); + if(external) + filename = nif->getString(); + else + { + nif->getChar(); // always 1 + data.read(nif); + } + + pixel = nif->getInt(); + mipmap = nif->getInt(); + alpha = nif->getInt(); + + nif->getChar(); // always 1 } void post(NIFFile *nif) { - Extra::post(nif); - controller.post(nif); - } -}; - -/// Has name, extra-data and controller -class Named : public Controlled -{ -public: - std::string name; - - void read(NIFStream *nif) - { - name = nif->getString(); - Controlled::read(nif); + Named::post(nif); + data.post(nif); } }; -typedef Named NiSequenceStreamHelper; class NiParticleGrowFade : public Controlled { @@ -147,5 +173,7 @@ public: } }; + + } // Namespace #endif diff --git a/components/nif/controller.hpp b/components/nif/controller.hpp index e44f4a6f3..9ae527e5a 100644 --- a/components/nif/controller.hpp +++ b/components/nif/controller.hpp @@ -24,44 +24,11 @@ #ifndef OPENMW_COMPONENTS_NIF_CONTROLLER_HPP #define OPENMW_COMPONENTS_NIF_CONTROLLER_HPP -#include "record.hpp" -#include "niffile.hpp" -#include "recordptr.hpp" +#include "base.hpp" namespace Nif { -class Controller : public Record -{ -public: - ControllerPtr next; - int flags; - float frequency, phase; - float timeStart, timeStop; - ControlledPtr target; - - void read(NIFStream *nif) - { - next.read(nif); - - flags = nif->getUShort(); - - frequency = nif->getFloat(); - phase = nif->getFloat(); - timeStart = nif->getFloat(); - timeStop = nif->getFloat(); - - target.read(nif); - } - - void post(NIFFile *nif) - { - Record::post(nif); - next.post(nif); - target.post(nif); - } -}; - class NiParticleSystemController : public Controller { public: @@ -89,7 +56,14 @@ public: float lifetime; float lifetimeRandom; - int emitFlags; // Bit 0: Emit Rate toggle bit (0 = auto adjust, 1 = use Emit Rate value) + enum EmitFlags + { + NoAutoAdjust = 0x1 // If this flag is set, we use the emitRate value. Otherwise, + // we calculate an emit rate so that the maximum number of particles + // in the system (numParticles) is never exceeded. + }; + int emitFlags; + Ogre::Vector3 offsetRandom; NodePtr emitter; diff --git a/components/nif/data.cpp b/components/nif/data.cpp new file mode 100644 index 000000000..4248b93d2 --- /dev/null +++ b/components/nif/data.cpp @@ -0,0 +1,29 @@ +#include "data.hpp" +#include "node.hpp" + +namespace Nif +{ +void NiSkinInstance::post(NIFFile *nif) +{ + data.post(nif); + root.post(nif); + bones.post(nif); + + if(data.empty() || root.empty()) + nif->fail("NiSkinInstance missing root or data"); + + size_t bnum = bones.length(); + if(bnum != data->bones.size()) + nif->fail("Mismatch in NiSkinData bone count"); + + root->makeRootBone(&data->trafo); + + for(size_t i=0; ifail("Oops: Missing bone! Don't know how to handle this."); + bones[i]->makeBone(i, data->bones[i]); + } +} + +} // Namespace diff --git a/components/nif/data.hpp b/components/nif/data.hpp index f1f34184b..f3b5a27f8 100644 --- a/components/nif/data.hpp +++ b/components/nif/data.hpp @@ -24,74 +24,11 @@ #ifndef OPENMW_COMPONENTS_NIF_DATA_HPP #define OPENMW_COMPONENTS_NIF_DATA_HPP -#include "controlled.hpp" - -#include -#include +#include "base.hpp" namespace Nif { -class NiSourceTexture : public Named -{ -public: - // Is this an external (references a separate texture file) or - // internal (data is inside the nif itself) texture? - bool external; - - std::string filename; // In case of external textures - NiPixelDataPtr data; // In case of internal textures - - /* Pixel layout - 0 - Palettised - 1 - High color 16 - 2 - True color 32 - 3 - Compressed - 4 - Bumpmap - 5 - Default */ - int pixel; - - /* Mipmap format - 0 - no - 1 - yes - 2 - default */ - int mipmap; - - /* Alpha - 0 - none - 1 - binary - 2 - smooth - 3 - default (use material alpha, or multiply material with texture if present) - */ - int alpha; - - void read(NIFStream *nif) - { - Named::read(nif); - - external = !!nif->getChar(); - if(external) - filename = nif->getString(); - else - { - nif->getChar(); // always 1 - data.read(nif); - } - - pixel = nif->getInt(); - mipmap = nif->getInt(); - alpha = nif->getInt(); - - nif->getChar(); // always 1 - } - - void post(NIFFile *nif) - { - Named::post(nif); - data.post(nif); - } -}; - // Common ancestor for several data classes class ShapeData : public Record { @@ -211,7 +148,7 @@ public: class NiPosData : public Record { public: - Vector3KeyList mKeyList; + Vector3KeyMap mKeyList; void read(NIFStream *nif) { @@ -222,7 +159,7 @@ public: class NiUVData : public Record { public: - FloatKeyList mKeyList[4]; + FloatKeyMap mKeyList[4]; void read(NIFStream *nif) { @@ -234,7 +171,7 @@ public: class NiFloatData : public Record { public: - FloatKeyList mKeyList; + FloatKeyMap mKeyList; void read(NIFStream *nif) { @@ -284,11 +221,11 @@ public: class NiColorData : public Record { public: - Vector4KeyList mKeyList; + Vector4KeyMap mKeyMap; void read(NIFStream *nif) { - mKeyList.read(nif); + mKeyMap.read(nif); } }; @@ -335,7 +272,7 @@ class NiSkinData : public Record public: struct BoneTrafo { - Ogre::Matrix3 rotation; // Rotation offset from bone? + Ogre::Matrix3 rotationScale; // Rotation offset from bone, non-uniform scale Ogre::Vector3 trans; // Translation float scale; // Probably scale (always 1) }; @@ -358,7 +295,7 @@ public: void read(NIFStream *nif) { - trafo.rotation = nif->getMatrix3(); + trafo.rotationScale = nif->getMatrix3(); trafo.trans = nif->getVector3(); trafo.scale = nif->getFloat(); @@ -370,7 +307,7 @@ public: { BoneInfo &bi = bones[i]; - bi.trafo.rotation = nif->getMatrix3(); + bi.trafo.rotationScale = nif->getMatrix3(); bi.trafo.trans = nif->getVector3(); bi.trafo.scale = nif->getFloat(); bi.unknown = nif->getVector4(); @@ -389,7 +326,7 @@ public: struct NiMorphData : public Record { struct MorphData { - FloatKeyList mData; + FloatKeyMap mData; std::vector mVertices; }; std::vector mMorphs; @@ -412,13 +349,26 @@ struct NiMorphData : public Record struct NiKeyframeData : public Record { - QuaternionKeyList mRotations; - Vector3KeyList mTranslations; - FloatKeyList mScales; + QuaternionKeyMap mRotations; + //\FIXME mXYZ_Keys are read, but not used. + FloatKeyMap mXYZ_Keys; + Vector3KeyMap mTranslations; + FloatKeyMap mScales; void read(NIFStream *nif) { mRotations.read(nif); + if(mRotations.mInterpolationType == mRotations.sXYZInterpolation) + { + //Chomp unused float + nif->getFloat(); + for(size_t i=0;i<3;++i) + { + //Read concatenates items together. + mXYZ_Keys.read(nif,true); + } + nif->file->warn("XYZ_ROTATION_KEY read, but not used!"); + } mTranslations.read(nif); mScales.read(nif); } diff --git a/components/nif/extra.hpp b/components/nif/extra.hpp index 45c4fefc6..2913c62b0 100644 --- a/components/nif/extra.hpp +++ b/components/nif/extra.hpp @@ -24,26 +24,11 @@ #ifndef OPENMW_COMPONENTS_NIF_EXTRA_HPP #define OPENMW_COMPONENTS_NIF_EXTRA_HPP -#include "record.hpp" -#include "niffile.hpp" -#include "recordptr.hpp" +#include "base.hpp" namespace Nif { -/** A record that can have extra data. The extra data objects - themselves decend from the Extra class, and all the extra data - connected to an object form a linked list -*/ -class Extra : public Record -{ -public: - ExtraPtr extra; - - void read(NIFStream *nif) { extra.read(nif); } - void post(NIFFile *nif) { extra.post(nif); } -}; - class NiVertWeightsExtraData : public Extra { public: diff --git a/components/nif/niffile.cpp b/components/nif/niffile.cpp index 0f7e658fb..c689e27b3 100644 --- a/components/nif/niffile.cpp +++ b/components/nif/niffile.cpp @@ -1,188 +1,27 @@ -/* - OpenMW - The completely unofficial reimplementation of Morrowind - Copyright (C) 2008-2010 Nicolay Korslund - Email: < korslund@gmail.com > - WWW: http://openmw.sourceforge.net/ - - This file (nif_file.cpp) is part of the OpenMW package. - - OpenMW is distributed as free software: you can redistribute it - and/or modify it under the terms of the GNU General Public License - version 3, as published by the Free Software Foundation. - - This program is distributed in the hope that it will be useful, but - WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - General Public License for more details. - - You should have received a copy of the GNU General Public License - version 3 along with this program. If not, see - http://www.gnu.org/licenses/ . - - */ - #include "niffile.hpp" -#include "record.hpp" -#include "components/misc/stringops.hpp" - -#include "extra.hpp" -#include "controlled.hpp" -#include "node.hpp" -#include "property.hpp" -#include "data.hpp" #include "effect.hpp" -#include "controller.hpp" -#include +#include -//TODO: when threading is needed, enable these -//#include -#include +#include namespace Nif { -class NIFFile::LoadedCache -{ - //TODO: enable this to make cache thread safe... - //typedef boost::mutex mutex; - - struct mutex - { - void lock () {}; - void unlock () {} - }; - - typedef boost::lock_guard lock_guard; - typedef std::map < std::string, boost::weak_ptr > loaded_map; - typedef std::vector < boost::shared_ptr > locked_files; - - static int sLockLevel; - static mutex sProtector; - static loaded_map sLoadedMap; - static locked_files sLockedFiles; - -public: - - static ptr create (const std::string &name) - { - lock_guard _ (sProtector); - - ptr result; - - // lookup the resource - loaded_map::iterator i = sLoadedMap.find (name); - - if (i == sLoadedMap.end ()) // it doesn't existing currently, - { // or hasn't in the very near past - - // create it now, for smoother threading if needed, the - // loading should be performed outside of the sLoaderMap - // lock and an alternate mechanism should be used to - // synchronize threads competing to load the same resource - result = boost::make_shared (name, psudo_private_modifier()); - - // if we are locking the cache add an extra reference - // to keep the file in memory - if (sLockLevel > 0) - sLockedFiles.push_back (result); - - // stash a reference to the resource so that future - // calls can benefit - sLoadedMap [name] = boost::weak_ptr (result); - } - else // it may (probably) still exists - { - // attempt to get the reference - result = i->second.lock (); - - if (!result) // resource is in the process of being destroyed - { - // create a new instance, to replace the one that has - // begun the irreversible process of being destroyed - result = boost::make_shared (name, psudo_private_modifier()); - - // respect the cache lock... - if (sLockLevel > 0) - sLockedFiles.push_back (result); - - // we potentially overwrite an expired pointer here - // but the other thread performing the delete on - // the previous copy of this resource will detect it - // and make sure not to erase the new reference - sLoadedMap [name] = boost::weak_ptr (result); - } - } - - // we made it! - return result; - } - - static void release (NIFFile * file) - { - lock_guard _ (sProtector); - - loaded_map::iterator i = sLoadedMap.find (file->filename); - - // its got to be in here, it just might not be us... - assert (i != sLoadedMap.end ()); - - // if weak_ptr is still expired, this resource hasn't been recreated - // between the initiation of the final release due to destruction - // of the last shared pointer and this thread acquiring the lock on - // the loader map - if (i->second.expired ()) - sLoadedMap.erase (i); - } - - static void lockCache () - { - lock_guard _ (sProtector); - - sLockLevel++; - } - - static void unlockCache () - { - locked_files resetList; - - { - lock_guard _ (sProtector); - - if (--sLockLevel) - sLockedFiles.swap(resetList); - } - - // this not necessary, but makes it clear that the - // deletion of the locked cache entries is being done - // outside the protection of sProtector - resetList.clear (); - } -}; - -int NIFFile::LoadedCache::sLockLevel = 0; -NIFFile::LoadedCache::mutex NIFFile::LoadedCache::sProtector; -NIFFile::LoadedCache::loaded_map NIFFile::LoadedCache::sLoadedMap; -NIFFile::LoadedCache::locked_files NIFFile::LoadedCache::sLockedFiles; - -// these three calls are forwarded to the cache implementation... -void NIFFile::lockCache () { LoadedCache::lockCache (); } -void NIFFile::unlockCache () { LoadedCache::unlockCache (); } -NIFFile::ptr NIFFile::create (const std::string &name) { return LoadedCache::create (name); } - /// Open a NIF stream. The name is used for error messages. -NIFFile::NIFFile(const std::string &name, psudo_private_modifier) - : filename(name) +NIFFile::NIFFile(const std::string &name) + : ver(0) + , filename(name) { parse(); } NIFFile::~NIFFile() { - LoadedCache::release (this); - - for(std::size_t i=0; i::iterator it = records.begin() ; it != records.end(); ++it) + { + delete *it; + } } template static Record* construct() { return new NodeType; } @@ -191,101 +30,102 @@ struct RecordFactoryEntry { typedef Record* (*create_t) (); - char const * mName; create_t mCreate; RecordType mType; }; -/* These are all the record types we know how to read. - - This can be heavily optimized later if needed. For example, a - hash table or a FSM-based parser could be used to look up - node names. -*/ +///Helper function for adding records to the factory map +static std::pair makeEntry(std::string recName, Record* (*create_t) (), RecordType type) +{ + RecordFactoryEntry anEntry = {create_t,type}; + return std::make_pair(recName, anEntry); +} -static const RecordFactoryEntry recordFactories [] = { +///These are all the record types we know how to read. +static std::map makeFactory() +{ + std::map newFactory; + newFactory.insert(makeEntry("NiNode", &construct , RC_NiNode )); + newFactory.insert(makeEntry("AvoidNode", &construct , RC_AvoidNode )); + newFactory.insert(makeEntry("NiBSParticleNode", &construct , RC_NiBSParticleNode )); + newFactory.insert(makeEntry("NiBSAnimationNode", &construct , RC_NiBSAnimationNode )); + newFactory.insert(makeEntry("NiBillboardNode", &construct , RC_NiBillboardNode )); + newFactory.insert(makeEntry("NiTriShape", &construct , RC_NiTriShape )); + newFactory.insert(makeEntry("NiRotatingParticles", &construct , RC_NiRotatingParticles )); + newFactory.insert(makeEntry("NiAutoNormalParticles", &construct , RC_NiAutoNormalParticles )); + newFactory.insert(makeEntry("NiCamera", &construct , RC_NiCamera )); + newFactory.insert(makeEntry("RootCollisionNode", &construct , RC_RootCollisionNode )); + newFactory.insert(makeEntry("NiTexturingProperty", &construct , RC_NiTexturingProperty )); + newFactory.insert(makeEntry("NiMaterialProperty", &construct , RC_NiMaterialProperty )); + newFactory.insert(makeEntry("NiZBufferProperty", &construct , RC_NiZBufferProperty )); + newFactory.insert(makeEntry("NiAlphaProperty", &construct , RC_NiAlphaProperty )); + newFactory.insert(makeEntry("NiVertexColorProperty", &construct , RC_NiVertexColorProperty )); + newFactory.insert(makeEntry("NiShadeProperty", &construct , RC_NiShadeProperty )); + newFactory.insert(makeEntry("NiDitherProperty", &construct , RC_NiDitherProperty )); + newFactory.insert(makeEntry("NiWireframeProperty", &construct , RC_NiWireframeProperty )); + newFactory.insert(makeEntry("NiSpecularProperty", &construct , RC_NiSpecularProperty )); + newFactory.insert(makeEntry("NiStencilProperty", &construct , RC_NiStencilProperty )); + newFactory.insert(makeEntry("NiVisController", &construct , RC_NiVisController )); + newFactory.insert(makeEntry("NiGeomMorpherController", &construct , RC_NiGeomMorpherController )); + newFactory.insert(makeEntry("NiKeyframeController", &construct , RC_NiKeyframeController )); + newFactory.insert(makeEntry("NiAlphaController", &construct , RC_NiAlphaController )); + newFactory.insert(makeEntry("NiUVController", &construct , RC_NiUVController )); + newFactory.insert(makeEntry("NiPathController", &construct , RC_NiPathController )); + newFactory.insert(makeEntry("NiMaterialColorController", &construct , RC_NiMaterialColorController )); + newFactory.insert(makeEntry("NiBSPArrayController", &construct , RC_NiBSPArrayController )); + newFactory.insert(makeEntry("NiParticleSystemController", &construct , RC_NiParticleSystemController )); + newFactory.insert(makeEntry("NiFlipController", &construct , RC_NiFlipController )); + newFactory.insert(makeEntry("NiAmbientLight", &construct , RC_NiLight )); + newFactory.insert(makeEntry("NiDirectionalLight", &construct , RC_NiLight )); + newFactory.insert(makeEntry("NiTextureEffect", &construct , RC_NiTextureEffect )); + newFactory.insert(makeEntry("NiVertWeightsExtraData", &construct , RC_NiVertWeightsExtraData )); + newFactory.insert(makeEntry("NiTextKeyExtraData", &construct , RC_NiTextKeyExtraData )); + newFactory.insert(makeEntry("NiStringExtraData", &construct , RC_NiStringExtraData )); + newFactory.insert(makeEntry("NiGravity", &construct , RC_NiGravity )); + newFactory.insert(makeEntry("NiPlanarCollider", &construct , RC_NiPlanarCollider )); + newFactory.insert(makeEntry("NiParticleGrowFade", &construct , RC_NiParticleGrowFade )); + newFactory.insert(makeEntry("NiParticleColorModifier", &construct , RC_NiParticleColorModifier )); + newFactory.insert(makeEntry("NiParticleRotation", &construct , RC_NiParticleRotation )); + newFactory.insert(makeEntry("NiFloatData", &construct , RC_NiFloatData )); + newFactory.insert(makeEntry("NiTriShapeData", &construct , RC_NiTriShapeData )); + newFactory.insert(makeEntry("NiVisData", &construct , RC_NiVisData )); + newFactory.insert(makeEntry("NiColorData", &construct , RC_NiColorData )); + newFactory.insert(makeEntry("NiPixelData", &construct , RC_NiPixelData )); + newFactory.insert(makeEntry("NiMorphData", &construct , RC_NiMorphData )); + newFactory.insert(makeEntry("NiKeyframeData", &construct , RC_NiKeyframeData )); + newFactory.insert(makeEntry("NiSkinData", &construct , RC_NiSkinData )); + newFactory.insert(makeEntry("NiUVData", &construct , RC_NiUVData )); + newFactory.insert(makeEntry("NiPosData", &construct , RC_NiPosData )); + newFactory.insert(makeEntry("NiRotatingParticlesData", &construct , RC_NiRotatingParticlesData )); + newFactory.insert(makeEntry("NiAutoNormalParticlesData", &construct , RC_NiAutoNormalParticlesData )); + newFactory.insert(makeEntry("NiSequenceStreamHelper", &construct , RC_NiSequenceStreamHelper )); + newFactory.insert(makeEntry("NiSourceTexture", &construct , RC_NiSourceTexture )); + newFactory.insert(makeEntry("NiSkinInstance", &construct , RC_NiSkinInstance )); + return newFactory; +} - { "NiNode", &construct , RC_NiNode }, - { "AvoidNode", &construct , RC_AvoidNode }, - { "NiBSParticleNode", &construct , RC_NiBSParticleNode }, - { "NiBSAnimationNode", &construct , RC_NiBSAnimationNode }, - { "NiBillboardNode", &construct , RC_NiBillboardNode }, - { "NiTriShape", &construct , RC_NiTriShape }, - { "NiRotatingParticles", &construct , RC_NiRotatingParticles }, - { "NiAutoNormalParticles", &construct , RC_NiAutoNormalParticles }, - { "NiCamera", &construct , RC_NiCamera }, - { "RootCollisionNode", &construct , RC_RootCollisionNode }, - { "NiTexturingProperty", &construct , RC_NiTexturingProperty }, - { "NiMaterialProperty", &construct , RC_NiMaterialProperty }, - { "NiZBufferProperty", &construct , RC_NiZBufferProperty }, - { "NiAlphaProperty", &construct , RC_NiAlphaProperty }, - { "NiVertexColorProperty", &construct , RC_NiVertexColorProperty }, - { "NiShadeProperty", &construct , RC_NiShadeProperty }, - { "NiDitherProperty", &construct , RC_NiDitherProperty }, - { "NiWireframeProperty", &construct , RC_NiWireframeProperty }, - { "NiSpecularProperty", &construct , RC_NiSpecularProperty }, - { "NiStencilProperty", &construct , RC_NiStencilProperty }, - { "NiVisController", &construct , RC_NiVisController }, - { "NiGeomMorpherController", &construct , RC_NiGeomMorpherController }, - { "NiKeyframeController", &construct , RC_NiKeyframeController }, - { "NiAlphaController", &construct , RC_NiAlphaController }, - { "NiUVController", &construct , RC_NiUVController }, - { "NiPathController", &construct , RC_NiPathController }, - { "NiMaterialColorController", &construct , RC_NiMaterialColorController }, - { "NiBSPArrayController", &construct , RC_NiBSPArrayController }, - { "NiParticleSystemController", &construct , RC_NiParticleSystemController }, - { "NiFlipController", &construct , RC_NiFlipController }, - { "NiAmbientLight", &construct , RC_NiLight }, - { "NiDirectionalLight", &construct , RC_NiLight }, - { "NiTextureEffect", &construct , RC_NiTextureEffect }, - { "NiVertWeightsExtraData", &construct , RC_NiVertWeightsExtraData }, - { "NiTextKeyExtraData", &construct , RC_NiTextKeyExtraData }, - { "NiStringExtraData", &construct , RC_NiStringExtraData }, - { "NiGravity", &construct , RC_NiGravity }, - { "NiPlanarCollider", &construct , RC_NiPlanarCollider }, - { "NiParticleGrowFade", &construct , RC_NiParticleGrowFade }, - { "NiParticleColorModifier", &construct , RC_NiParticleColorModifier }, - { "NiParticleRotation", &construct , RC_NiParticleRotation }, - { "NiFloatData", &construct , RC_NiFloatData }, - { "NiTriShapeData", &construct , RC_NiTriShapeData }, - { "NiVisData", &construct , RC_NiVisData }, - { "NiColorData", &construct , RC_NiColorData }, - { "NiPixelData", &construct , RC_NiPixelData }, - { "NiMorphData", &construct , RC_NiMorphData }, - { "NiKeyframeData", &construct , RC_NiKeyframeData }, - { "NiSkinData", &construct , RC_NiSkinData }, - { "NiUVData", &construct , RC_NiUVData }, - { "NiPosData", &construct , RC_NiPosData }, - { "NiRotatingParticlesData", &construct , RC_NiRotatingParticlesData }, - { "NiAutoNormalParticlesData", &construct , RC_NiAutoNormalParticlesData }, - { "NiSequenceStreamHelper", &construct , RC_NiSequenceStreamHelper }, - { "NiSourceTexture", &construct , RC_NiSourceTexture }, - { "NiSkinInstance", &construct , RC_NiSkinInstance }, -}; -static RecordFactoryEntry const * recordFactories_begin = &recordFactories [0]; -static RecordFactoryEntry const * recordFactories_end = &recordFactories [sizeof (recordFactories) / sizeof (recordFactories[0])]; +///Make the factory map used for parsing the file +static const std::map factories = makeFactory(); -RecordFactoryEntry const * lookupRecordFactory (char const * name) +/// Get the file's version in a human readable form +std::string NIFFile::printVersion(unsigned int version) { - RecordFactoryEntry const * i; - - for (i = recordFactories_begin; i != recordFactories_end; ++i) - if (strcmp (name, i->mName) == 0) - break; + union ver_quad + { + uint32_t full; + uint8_t quad[4]; + } version_out; - if (i == recordFactories_end) - return NULL; + version_out.full = version; - return i; + return Ogre::StringConverter::toString(version_out.quad[3]) + +"." + Ogre::StringConverter::toString(version_out.quad[2]) + +"." + Ogre::StringConverter::toString(version_out.quad[1]) + +"." + Ogre::StringConverter::toString(version_out.quad[0]); } -/* This file implements functions from the NIFFile class. It is also - where we stash all the functions we couldn't add as inline - definitions in the record types. - */ - void NIFFile::parse() { NIFStream nif (this, Ogre::ResourceGroupManager::getSingleton().openResource(filename)); @@ -296,10 +136,9 @@ void NIFFile::parse() fail("Invalid NIF header"); // Get BCD version - ver = nif.getInt(); + ver = nif.getUInt(); if(ver != VER_MW) - fail("Unsupported NIF version"); - + fail("Unsupported NIF version: " + printVersion(ver)); // Number of records size_t recNum = nif.getInt(); records.resize(recNum); @@ -318,13 +157,16 @@ void NIFFile::parse() Record *r = NULL; std::string rec = nif.getString(); + if(rec.empty()) + fail("Record number " + Ogre::StringConverter::toString(i) + " out of " + Ogre::StringConverter::toString(recNum) + " is blank."); + - RecordFactoryEntry const * entry = lookupRecordFactory (rec.c_str ()); + std::map::const_iterator entry = factories.find(rec); - if (entry != NULL) + if (entry != factories.end()) { - r = entry->mCreate (); - r->recType = entry->mType; + r = entry->second.mCreate (); + r->recType = entry->second.mType; } else fail("Unknown record type " + rec); @@ -335,24 +177,24 @@ void NIFFile::parse() r->recIndex = i; records[i] = r; r->read(&nif); - - // Discard tranformations for the root node, otherwise some meshes - // occasionally get wrong orientation. Only for NiNode-s for now, but - // can be expanded if needed. - // This should be rewritten when the method is cleaned up. - if (0 == i && rec == "NiNode") - { - static_cast(r)->trafo = Nif::Transformation::getIdentity(); - } } size_t rootNum = nif.getUInt(); roots.resize(rootNum); + //Determine which records are roots for(size_t i = 0;i < rootNum;i++) { - intptr_t idx = nif.getInt(); - roots[i] = ((idx >= 0) ? records.at(idx) : NULL); + int idx = nif.getInt(); + if (idx >= 0) + { + roots[i] = records.at(idx); + } + else + { + roots[i] = NULL; + warn("Null Root found"); + } } // Once parsing is done, do post-processing. @@ -360,81 +202,4 @@ void NIFFile::parse() records[i]->post(this); } -/// \todo move to the write cpp file - -void NiSkinInstance::post(NIFFile *nif) -{ - data.post(nif); - root.post(nif); - bones.post(nif); - - if(data.empty() || root.empty()) - nif->fail("NiSkinInstance missing root or data"); - - size_t bnum = bones.length(); - if(bnum != data->bones.size()) - nif->fail("Mismatch in NiSkinData bone count"); - - root->makeRootBone(&data->trafo); - - for(size_t i=0; ifail("Oops: Missing bone! Don't know how to handle this."); - bones[i]->makeBone(i, data->bones[i]); - } -} - - -void Node::getProperties(const Nif::NiTexturingProperty *&texprop, - const Nif::NiMaterialProperty *&matprop, - const Nif::NiAlphaProperty *&alphaprop, - const Nif::NiVertexColorProperty *&vertprop, - const Nif::NiZBufferProperty *&zprop, - const Nif::NiSpecularProperty *&specprop, - const Nif::NiWireframeProperty *&wireprop) const -{ - if(parent) - parent->getProperties(texprop, matprop, alphaprop, vertprop, zprop, specprop, wireprop); - - for(size_t i = 0;i < props.length();i++) - { - // Entries may be empty - if(props[i].empty()) - continue; - - const Nif::Property *pr = props[i].getPtr(); - if(pr->recType == Nif::RC_NiTexturingProperty) - texprop = static_cast(pr); - else if(pr->recType == Nif::RC_NiMaterialProperty) - matprop = static_cast(pr); - else if(pr->recType == Nif::RC_NiAlphaProperty) - alphaprop = static_cast(pr); - else if(pr->recType == Nif::RC_NiVertexColorProperty) - vertprop = static_cast(pr); - else if(pr->recType == Nif::RC_NiZBufferProperty) - zprop = static_cast(pr); - else if(pr->recType == Nif::RC_NiSpecularProperty) - specprop = static_cast(pr); - else if(pr->recType == Nif::RC_NiWireframeProperty) - wireprop = static_cast(pr); - else - std::cerr<< "Unhandled property type: "<recName <getWorldTransform() * getLocalTransform(); - return getLocalTransform(); -} - } diff --git a/components/nif/niffile.hpp b/components/nif/niffile.hpp index 77e0acb9e..2ef2a6fda 100644 --- a/components/nif/niffile.hpp +++ b/components/nif/niffile.hpp @@ -1,52 +1,13 @@ -/* - OpenMW - The completely unofficial reimplementation of Morrowind - Copyright (C) 2008-2010 Nicolay Korslund - Email: < korslund@gmail.com > - WWW: http://openmw.sourceforge.net/ - - This file (nif_file.h) is part of the OpenMW package. - - OpenMW is distributed as free software: you can redistribute it - and/or modify it under the terms of the GNU General Public License - version 3, as published by the Free Software Foundation. - - This program is distributed in the hope that it will be useful, but - WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - General Public License for more details. - - You should have received a copy of the GNU General Public License - version 3 along with this program. If not, see - http://www.gnu.org/licenses/ . - - */ +///Main header for reading .nif files #ifndef OPENMW_COMPONENTS_NIF_NIFFILE_HPP #define OPENMW_COMPONENTS_NIF_NIFFILE_HPP -#include -#include -#include -#include -#include -#include -#include -#include - #include #include -#include - -#include -#include -#include -#include - -#include +#include #include "record.hpp" -#include "niftypes.hpp" -#include "nifstream.hpp" namespace Nif { @@ -58,193 +19,71 @@ class NIFFile }; /// Nif file version - int ver; + unsigned int ver; - /// File name, used for error messages + /// File name, used for error messages and opening the file std::string filename; /// Record list std::vector records; - /// Root list + /// Root list. This is a select portion of the pointers from records std::vector roots; /// Parse the file void parse(); - class LoadedCache; - friend class LoadedCache; + /// Get the file's version in a human readable form + ///\returns A string containing a human readable NIF version number + std::string printVersion(unsigned int version); - // attempt to protect NIFFile from misuse... - struct psudo_private_modifier {}; // this dirty little trick should optimize out + ///Private Copy Constructor NIFFile (NIFFile const &); + ///\overload void operator = (NIFFile const &); public: - /// Used for error handling + /// Used if file parsing fails void fail(const std::string &msg) { std::string err = "NIFFile Error: " + msg; err += "\nFile: " + filename; throw std::runtime_error(err); } - + /// Used when something goes wrong, but not catastrophically so void warn(const std::string &msg) { std::cerr << "NIFFile Warning: " << msg < ptr; - - /// Open a NIF stream. The name is used for error messages. - NIFFile(const std::string &name, psudo_private_modifier); + /// Open a NIF stream. The name is used for error messages and opening the file. + NIFFile(const std::string &name); ~NIFFile(); - static ptr create (const std::string &name); - static void lockCache (); - static void unlockCache (); - - struct CacheLock - { - CacheLock () { lockCache (); } - ~CacheLock () { unlockCache (); } - }; - /// Get a given record - Record *getRecord(size_t index) + Record *getRecord(size_t index) const { Record *res = records.at(index); - assert(res != NULL); return res; } /// Number of records - size_t numRecords() { return records.size(); } + size_t numRecords() const { return records.size(); } /// Get a given root - Record *getRoot(size_t index=0) + Record *getRoot(size_t index=0) const { Record *res = roots.at(index); - assert(res != NULL); return res; } /// Number of roots - size_t numRoots() { return roots.size(); } -}; - + size_t numRoots() const { return roots.size(); } -template -struct KeyT { - float mTime; - T mValue; - T mForwardValue; // Only for Quadratic interpolation - T mBackwardValue; // Only for Quadratic interpolation - float mTension; // Only for TBC interpolation - float mBias; // Only for TBC interpolation - float mContinuity; // Only for TBC interpolation + /// Get the name of the file + std::string getFilename(){ return filename; } }; -typedef KeyT FloatKey; -typedef KeyT Vector3Key; -typedef KeyT Vector4Key; -typedef KeyT QuaternionKey; - -template -struct KeyListT { - typedef std::vector< KeyT > VecType; - static const int sLinearInterpolation = 1; - static const int sQuadraticInterpolation = 2; - static const int sTBCInterpolation = 3; - static const int sXYZInterpolation = 4; - int mInterpolationType; - VecType mKeys; - - void read(NIFStream *nif, bool force=false) - { - size_t count = nif->getInt(); - if(count == 0 && !force) - return; - - mInterpolationType = nif->getInt(); - mKeys.resize(count); - if(mInterpolationType == sLinearInterpolation) - { - for(size_t i = 0;i < count;i++) - { - KeyT &key = mKeys[i]; - key.mTime = nif->getFloat(); - key.mValue = (nif->*getValue)(); - } - } - else if(mInterpolationType == sQuadraticInterpolation) - { - for(size_t i = 0;i < count;i++) - { - KeyT &key = mKeys[i]; - key.mTime = nif->getFloat(); - key.mValue = (nif->*getValue)(); - key.mForwardValue = (nif->*getValue)(); - key.mBackwardValue = (nif->*getValue)(); - } - } - else if(mInterpolationType == sTBCInterpolation) - { - for(size_t i = 0;i < count;i++) - { - KeyT &key = mKeys[i]; - key.mTime = nif->getFloat(); - key.mValue = (nif->*getValue)(); - key.mTension = nif->getFloat(); - key.mBias = nif->getFloat(); - key.mContinuity = nif->getFloat(); - } - } - //\FIXME This now reads the correct amount of data in the file, but doesn't actually do anything with it. - else if(mInterpolationType == sXYZInterpolation) - { - if (count != 1) - { - nif->file->fail("count should always be '1' for XYZ_ROTATION_KEY. Retrieved Value: "+Ogre::StringConverter::toString(count)); - return; - } - //KeyGroup (see http://niftools.sourceforge.net/doc/nif/NiKeyframeData.html) - //Chomp unknown and possibly unused float - nif->getFloat(); - for(size_t i=0;i<3;++i) - { - unsigned int numKeys = nif->getInt(); - if(numKeys != 0) - { - int interpolationTypeAgain = nif->getInt(); - if( interpolationTypeAgain != sLinearInterpolation) - { - nif->file->fail("XYZ_ROTATION_KEY's KeyGroup keyType must be '1' (Linear Interpolation). Retrieved Value: "+Ogre::StringConverter::toString(interpolationTypeAgain)); - return; - } - for(size_t j = 0;j < numKeys;j++) - { - //For now just chomp these - nif->getFloat(); - nif->getFloat(); - } - } - nif->file->warn("XYZ_ROTATION_KEY read, but not used!"); - } - } - else if (mInterpolationType == 0) - { - if (count != 0) - nif->file->fail("Interpolation type 0 doesn't work with keys"); - } - else - nif->file->fail("Unhandled interpolation type: "+Ogre::StringConverter::toString(mInterpolationType)); - } -}; -typedef KeyListT FloatKeyList; -typedef KeyListT Vector3KeyList; -typedef KeyListT Vector4KeyList; -typedef KeyListT QuaternionKeyList; } // Namespace #endif diff --git a/components/nif/nifkey.hpp b/components/nif/nifkey.hpp new file mode 100644 index 000000000..b0db80914 --- /dev/null +++ b/components/nif/nifkey.hpp @@ -0,0 +1,141 @@ +///File to handle keys used by nif file records + +#ifndef OPENMW_COMPONENTS_NIF_NIFKEY_HPP +#define OPENMW_COMPONENTS_NIF_NIFKEY_HPP + +#include + +#include "nifstream.hpp" + +namespace Nif +{ + +template +struct KeyT { + T mValue; + T mForwardValue; // Only for Quadratic interpolation, and never for QuaternionKeyList + T mBackwardValue; // Only for Quadratic interpolation, and never for QuaternionKeyList + float mTension; // Only for TBC interpolation + float mBias; // Only for TBC interpolation + float mContinuity; // Only for TBC interpolation +}; +typedef KeyT FloatKey; +typedef KeyT Vector3Key; +typedef KeyT Vector4Key; +typedef KeyT QuaternionKey; + +template +struct KeyMapT { + typedef std::map< float, KeyT > MapType; + + static const unsigned int sLinearInterpolation = 1; + static const unsigned int sQuadraticInterpolation = 2; + static const unsigned int sTBCInterpolation = 3; + static const unsigned int sXYZInterpolation = 4; + + unsigned int mInterpolationType; + MapType mKeys; + + KeyMapT() : mInterpolationType(sLinearInterpolation) {} + + //Read in a KeyGroup (see http://niftools.sourceforge.net/doc/nif/NiKeyframeData.html) + void read(NIFStream *nif, bool force=false) + { + assert(nif); + + mInterpolationType = 0; + + size_t count = nif->getUInt(); + if(count == 0 && !force) + return; + + //If we aren't forcing things, make sure that read clears any previous keys + if(!force) + mKeys.clear(); + + mInterpolationType = nif->getUInt(); + + KeyT key; + NIFStream &nifReference = *nif; + + if(mInterpolationType == sLinearInterpolation) + { + for(size_t i = 0;i < count;i++) + { + float time = nif->getFloat(); + readValue(nifReference, key); + mKeys[time] = key; + } + } + else if(mInterpolationType == sQuadraticInterpolation) + { + for(size_t i = 0;i < count;i++) + { + float time = nif->getFloat(); + readQuadratic(nifReference, key); + mKeys[time] = key; + } + } + else if(mInterpolationType == sTBCInterpolation) + { + for(size_t i = 0;i < count;i++) + { + float time = nif->getFloat(); + readTBC(nifReference, key); + mKeys[time] = key; + } + } + //XYZ keys aren't actually read here. + //data.hpp sees that the last type read was sXYZInterpolation and: + // Eats a floating point number, then + // Re-runs the read function 3 more times, with force enabled so that the previous values aren't cleared. + // When it does that it's reading in a bunch of sLinearInterpolation keys, not sXYZInterpolation. + else if(mInterpolationType == sXYZInterpolation) + { + //Don't try to read XYZ keys into the wrong part + if ( count != 1 ) + nif->file->fail("XYZ_ROTATION_KEY count should always be '1' . Retrieved Value: "+Ogre::StringConverter::toString(count)); + } + else if (0 == mInterpolationType) + { + if (count != 0) + nif->file->fail("Interpolation type 0 doesn't work with keys"); + } + else + nif->file->fail("Unhandled interpolation type: "+Ogre::StringConverter::toString(mInterpolationType)); + } + +private: + static void readValue(NIFStream &nif, KeyT &key) + { + key.mValue = (nif.*getValue)(); + } + + static void readQuadratic(NIFStream &nif, KeyT &key) + { + readValue(nif, key); + } + + template + static void readQuadratic(NIFStream &nif, KeyT &key) + { + readValue(nif, key); + key.mForwardValue = (nif.*getValue)(); + key.mBackwardValue = (nif.*getValue)(); + } + + static void readTBC(NIFStream &nif, KeyT &key) + { + readValue(nif, key); + key.mTension = nif.getFloat(); + key.mBias = nif.getFloat(); + key.mContinuity = nif.getFloat(); + } +}; +typedef KeyMapT FloatKeyMap; +typedef KeyMapT Vector3KeyMap; +typedef KeyMapT Vector4KeyMap; +typedef KeyMapT QuaternionKeyMap; + +} // Namespace +#endif //#ifndef OPENMW_COMPONENTS_NIF_NIFKEY_HPP diff --git a/components/nif/nifstream.cpp b/components/nif/nifstream.cpp new file mode 100644 index 000000000..527d1a2af --- /dev/null +++ b/components/nif/nifstream.cpp @@ -0,0 +1,137 @@ +#include "nifstream.hpp" +//For error reporting +#include "niffile.hpp" + +namespace Nif +{ + +//Private functions +uint8_t NIFStream::read_byte() +{ + uint8_t byte; + if(inp->read(&byte, 1) != 1) return 0; + return byte; +} +uint16_t NIFStream::read_le16() +{ + uint8_t buffer[2]; + if(inp->read(buffer, 2) != 2) return 0; + return buffer[0] | (buffer[1]<<8); +} +uint32_t NIFStream::read_le32() +{ + uint8_t buffer[4]; + if(inp->read(buffer, 4) != 4) return 0; + return buffer[0] | (buffer[1]<<8) | (buffer[2]<<16) | (buffer[3]<<24); +} +float NIFStream::read_le32f() +{ + union { + uint32_t i; + float f; + } u = { read_le32() }; + return u.f; +} + +//Public functions +Ogre::Vector2 NIFStream::getVector2() +{ + float a[2]; + for(size_t i = 0;i < 2;i++) + a[i] = getFloat(); + return Ogre::Vector2(a); +} +Ogre::Vector3 NIFStream::getVector3() +{ + float a[3]; + for(size_t i = 0;i < 3;i++) + a[i] = getFloat(); + return Ogre::Vector3(a); +} +Ogre::Vector4 NIFStream::getVector4() +{ + float a[4]; + for(size_t i = 0;i < 4;i++) + a[i] = getFloat(); + return Ogre::Vector4(a); +} +Ogre::Matrix3 NIFStream::getMatrix3() +{ + Ogre::Real a[3][3]; + for(size_t i = 0;i < 3;i++) + { + for(size_t j = 0;j < 3;j++) + a[i][j] = Ogre::Real(getFloat()); + } + return Ogre::Matrix3(a); +} +Ogre::Quaternion NIFStream::getQuaternion() +{ + float a[4]; + for(size_t i = 0;i < 4;i++) + a[i] = getFloat(); + return Ogre::Quaternion(a); +} +Transformation NIFStream::getTrafo() +{ + Transformation t; + t.pos = getVector3(); + t.rotationScale = getMatrix3(); + t.scale = getFloat(); + return t; +} + +std::string NIFStream::getString(size_t length) +{ + std::vector str (length+1, 0); + + if(inp->read(&str[0], length) != length) + throw std::runtime_error (": String length in NIF file "+ file->getFilename() +" does not match! Expected length: " + + Ogre::StringConverter::toString(length)); + + return &str[0]; +} +std::string NIFStream::getString() +{ + size_t size = read_le32(); + return getString(size); +} + +void NIFStream::getShorts(std::vector &vec, size_t size) +{ + vec.resize(size); + for(size_t i = 0;i < vec.size();i++) + vec[i] = getShort(); +} +void NIFStream::getFloats(std::vector &vec, size_t size) +{ + vec.resize(size); + for(size_t i = 0;i < vec.size();i++) + vec[i] = getFloat(); +} +void NIFStream::getVector2s(std::vector &vec, size_t size) +{ + vec.resize(size); + for(size_t i = 0;i < vec.size();i++) + vec[i] = getVector2(); +} +void NIFStream::getVector3s(std::vector &vec, size_t size) +{ + vec.resize(size); + for(size_t i = 0;i < vec.size();i++) + vec[i] = getVector3(); +} +void NIFStream::getVector4s(std::vector &vec, size_t size) +{ + vec.resize(size); + for(size_t i = 0;i < vec.size();i++) + vec[i] = getVector4(); +} +void NIFStream::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/nif/nifstream.hpp b/components/nif/nifstream.hpp index a2595d17b..3d6a1319a 100644 --- a/components/nif/nifstream.hpp +++ b/components/nif/nifstream.hpp @@ -1,6 +1,21 @@ +///Functions used to read raw binary data from .nif files + #ifndef OPENMW_COMPONENTS_NIF_NIFSTREAM_HPP #define OPENMW_COMPONENTS_NIF_NIFSTREAM_HPP +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "niftypes.hpp" + namespace Nif { @@ -11,32 +26,10 @@ class NIFStream { /// Input stream Ogre::DataStreamPtr inp; - uint8_t read_byte() - { - uint8_t byte; - if(inp->read(&byte, 1) != 1) return 0; - return byte; - } - uint16_t read_le16() - { - uint8_t buffer[2]; - if(inp->read(buffer, 2) != 2) return 0; - return buffer[0] | (buffer[1]<<8); - } - uint32_t read_le32() - { - uint8_t buffer[4]; - if(inp->read(buffer, 4) != 4) return 0; - return buffer[0] | (buffer[1]<<8) | (buffer[2]<<16) | (buffer[3]<<24); - } - float read_le32f() - { - union { - uint32_t i; - float f; - } u = { read_le32() }; - return u.f; - } + uint8_t read_byte(); + uint16_t read_le16(); + uint32_t read_le32(); + float read_le32f(); public: @@ -74,106 +67,27 @@ public: short getShort() { return read_le16(); } unsigned short getUShort() { return read_le16(); } int getInt() { return read_le32(); } - int getUInt() { return read_le32(); } + unsigned int getUInt() { return read_le32(); } float getFloat() { return read_le32f(); } - Ogre::Vector2 getVector2() - { - float a[2]; - for(size_t i = 0;i < 2;i++) - a[i] = getFloat(); - return Ogre::Vector2(a); - } - Ogre::Vector3 getVector3() - { - float a[3]; - for(size_t i = 0;i < 3;i++) - a[i] = getFloat(); - return Ogre::Vector3(a); - } - Ogre::Vector4 getVector4() - { - float a[4]; - for(size_t i = 0;i < 4;i++) - a[i] = getFloat(); - return Ogre::Vector4(a); - } - Ogre::Matrix3 getMatrix3() - { - Ogre::Real a[3][3]; - for(size_t i = 0;i < 3;i++) - { - for(size_t j = 0;j < 3;j++) - a[i][j] = Ogre::Real(getFloat()); - } - return Ogre::Matrix3(a); - } - Ogre::Quaternion getQuaternion() - { - float a[4]; - for(size_t i = 0;i < 4;i++) - a[i] = getFloat(); - return Ogre::Quaternion(a); - } - Transformation getTrafo() - { - Transformation t; - t.pos = getVector3(); - t.rotation = getMatrix3(); - t.scale = getFloat(); - return t; - } - - std::string getString(size_t length) - { - std::vector str (length+1, 0); - - if(inp->read(&str[0], length) != length) - throw std::runtime_error ("string length in NIF file does not match"); - return &str[0]; - } - std::string getString() - { - size_t size = read_le32(); - return getString(size); - } - - void getShorts(std::vector &vec, size_t size) - { - vec.resize(size); - for(size_t i = 0;i < vec.size();i++) - vec[i] = getShort(); - } - void getFloats(std::vector &vec, size_t size) - { - vec.resize(size); - for(size_t i = 0;i < vec.size();i++) - vec[i] = getFloat(); - } - void getVector2s(std::vector &vec, size_t size) - { - vec.resize(size); - for(size_t i = 0;i < vec.size();i++) - vec[i] = getVector2(); - } - void getVector3s(std::vector &vec, size_t size) - { - vec.resize(size); - for(size_t i = 0;i < vec.size();i++) - vec[i] = getVector3(); - } - void getVector4s(std::vector &vec, size_t size) - { - vec.resize(size); - for(size_t i = 0;i < vec.size();i++) - vec[i] = getVector4(); - } - void getQuaternions(std::vector &quat, size_t size) - { - quat.resize(size); - for(size_t i = 0;i < quat.size();i++) - quat[i] = getQuaternion(); - } + Ogre::Vector2 getVector2(); + Ogre::Vector3 getVector3(); + Ogre::Vector4 getVector4(); + Ogre::Matrix3 getMatrix3(); + Ogre::Quaternion getQuaternion(); + Transformation getTrafo(); + + ///Read in a string of the given length + std::string getString(size_t length); + ///Read in a string of the length specified in the file + std::string getString(); + + void getShorts(std::vector &vec, size_t size); + void getFloats(std::vector &vec, size_t size); + void getVector2s(std::vector &vec, size_t size); + void getVector3s(std::vector &vec, size_t size); + void getVector4s(std::vector &vec, size_t size); + void getQuaternions(std::vector &quat, size_t size); }; } diff --git a/components/nif/niftypes.hpp b/components/nif/niftypes.hpp index 786c48b65..f9235ec45 100644 --- a/components/nif/niftypes.hpp +++ b/components/nif/niftypes.hpp @@ -35,7 +35,7 @@ namespace Nif struct Transformation { Ogre::Vector3 pos; - Ogre::Matrix3 rotation; + Ogre::Matrix3 rotationScale; float scale; static const Transformation& getIdentity() diff --git a/components/nif/node.cpp b/components/nif/node.cpp new file mode 100644 index 000000000..7529e602d --- /dev/null +++ b/components/nif/node.cpp @@ -0,0 +1,58 @@ +#include "node.hpp" + +namespace Nif +{ + +void Node::getProperties(const Nif::NiTexturingProperty *&texprop, + const Nif::NiMaterialProperty *&matprop, + const Nif::NiAlphaProperty *&alphaprop, + const Nif::NiVertexColorProperty *&vertprop, + const Nif::NiZBufferProperty *&zprop, + const Nif::NiSpecularProperty *&specprop, + const Nif::NiWireframeProperty *&wireprop) const +{ + if(parent) + parent->getProperties(texprop, matprop, alphaprop, vertprop, zprop, specprop, wireprop); + + for(size_t i = 0;i < props.length();i++) + { + // Entries may be empty + if(props[i].empty()) + continue; + + const Nif::Property *pr = props[i].getPtr(); + if(pr->recType == Nif::RC_NiTexturingProperty) + texprop = static_cast(pr); + else if(pr->recType == Nif::RC_NiMaterialProperty) + matprop = static_cast(pr); + else if(pr->recType == Nif::RC_NiAlphaProperty) + alphaprop = static_cast(pr); + else if(pr->recType == Nif::RC_NiVertexColorProperty) + vertprop = static_cast(pr); + else if(pr->recType == Nif::RC_NiZBufferProperty) + zprop = static_cast(pr); + else if(pr->recType == Nif::RC_NiSpecularProperty) + specprop = static_cast(pr); + else if(pr->recType == Nif::RC_NiWireframeProperty) + wireprop = static_cast(pr); + else + std::cerr<< "Unhandled property type: "<recName <getWorldTransform() * getLocalTransform(); + return getLocalTransform(); +} + +} diff --git a/components/nif/node.hpp b/components/nif/node.hpp index eebcd8be8..c8c0b6db9 100644 --- a/components/nif/node.hpp +++ b/components/nif/node.hpp @@ -1,34 +1,15 @@ -/* - OpenMW - The completely unofficial reimplementation of Morrowind - Copyright (C) 2008-2010 Nicolay Korslund - Email: < korslund@gmail.com > - WWW: http://openmw.sourceforge.net/ - - This file (node.h) is part of the OpenMW package. - - OpenMW is distributed as free software: you can redistribute it - and/or modify it under the terms of the GNU General Public License - version 3, as published by the Free Software Foundation. - - This program is distributed in the hope that it will be useful, but - WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - General Public License for more details. - - You should have received a copy of the GNU General Public License - version 3 along with this program. If not, see - http://www.gnu.org/licenses/ . - - */ - #ifndef OPENMW_COMPONENTS_NIF_NODE_HPP #define OPENMW_COMPONENTS_NIF_NODE_HPP #include #include "controlled.hpp" +#include "extra.hpp" #include "data.hpp" #include "property.hpp" +#include "niftypes.hpp" +#include "controller.hpp" +#include "base.hpp" namespace Nif { @@ -149,6 +130,14 @@ struct NiNode : Node Node::read(nif); children.read(nif); effects.read(nif); + + // Discard tranformations for the root node, otherwise some meshes + // occasionally get wrong orientation. Only for NiNode-s for now, but + // can be expanded if needed. + if (0 == recIndex) + { + static_cast(this)->trafo = Nif::Transformation::getIdentity(); + } } void post(NIFFile *nif) diff --git a/components/nif/property.hpp b/components/nif/property.hpp index 06c8260ce..2c7747a3e 100644 --- a/components/nif/property.hpp +++ b/components/nif/property.hpp @@ -24,7 +24,7 @@ #ifndef OPENMW_COMPONENTS_NIF_PROPERTY_HPP #define OPENMW_COMPONENTS_NIF_PROPERTY_HPP -#include "controlled.hpp" +#include "base.hpp" namespace Nif { diff --git a/components/nif/recordptr.hpp b/components/nif/recordptr.hpp index 1a4fc235b..5eac277d0 100644 --- a/components/nif/recordptr.hpp +++ b/components/nif/recordptr.hpp @@ -1,30 +1,8 @@ -/* - OpenMW - The completely unofficial reimplementation of Morrowind - Copyright (C) 2008-2010 Nicolay Korslund - Email: < korslund@gmail.com > - WWW: http://openmw.sourceforge.net/ - - This file (record_ptr.h) is part of the OpenMW package. - - OpenMW is distributed as free software: you can redistribute it - and/or modify it under the terms of the GNU General Public License - version 3, as published by the Free Software Foundation. - - This program is distributed in the hope that it will be useful, but - WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - General Public License for more details. - - You should have received a copy of the GNU General Public License - version 3 along with this program. If not, see - http://www.gnu.org/licenses/ . - - */ - #ifndef OPENMW_COMPONENTS_NIF_RECORDPTR_HPP #define OPENMW_COMPONENTS_NIF_RECORDPTR_HPP #include "niffile.hpp" +#include "nifstream.hpp" #include namespace Nif diff --git a/components/nif/tests/.gitignore b/components/nif/tests/.gitignore index b01c11f27..397b4a762 100644 --- a/components/nif/tests/.gitignore +++ b/components/nif/tests/.gitignore @@ -1,5 +1 @@ -niftool -*_test -*.nif -*.kf -output.txt +*.log diff --git a/components/nif/tests/CMakeLists.txt b/components/nif/tests/CMakeLists.txt new file mode 100644 index 000000000..a45298180 --- /dev/null +++ b/components/nif/tests/CMakeLists.txt @@ -0,0 +1,19 @@ +set(NIFTEST + niftest.cpp +) +source_group(components\\nif\\tests FILES ${NIFTEST}) + +# Main executable +add_executable(niftest + ${NIFTEST} +) + +target_link_libraries(niftest + ${Boost_LIBRARIES} + components +) + +if (BUILD_WITH_CODE_COVERAGE) + add_definitions (--coverage) + target_link_libraries(niftest gcov) +endif() diff --git a/components/nif/tests/Makefile b/components/nif/tests/Makefile deleted file mode 100644 index 0754bdfa6..000000000 --- a/components/nif/tests/Makefile +++ /dev/null @@ -1,12 +0,0 @@ -GCC=g++ - -all: niftool nif_bsa_test - -niftool: niftool.cpp ../nif_file.hpp ../nif_file.cpp ../record.hpp - $(GCC) $< ../nif_file.cpp ../../tools/stringops.cpp -o $@ - -nif_bsa_test: nif_bsa_test.cpp ../nif_file.cpp ../../bsa/bsa_file.cpp ../../tools/stringops.cpp - $(GCC) $^ -o $@ - -clean: - rm niftool *_test diff --git a/components/nif/tests/nif_bsa_test.cpp b/components/nif/tests/nif_bsa_test.cpp deleted file mode 100644 index c22aad680..000000000 --- a/components/nif/tests/nif_bsa_test.cpp +++ /dev/null @@ -1,30 +0,0 @@ -/* - Runs NIFFile through all the NIFs in Morrowind.bsa. - */ - -#include "../nif_file.hpp" -#include "../../bsa/bsa_file.hpp" -#include "../../tools/stringops.hpp" -#include - -using namespace Mangle::Stream; -using namespace std; -using namespace Nif; - -int main(int argc, char **args) -{ - BSAFile bsa; - cout << "Reading Morrowind.bsa\n"; - bsa.open("../../data/Morrowind.bsa"); - - const BSAFile::FileList &files = bsa.getList(); - - for(int i=0; i +#include +#include +#include +#include + +///See if the file has the named extension +bool hasExtension(std::string filename, std::string extensionToFind) +{ + std::string extension = filename.substr(filename.find_last_of(".")+1); + + //Convert strings to lower case for comparison + std::transform(extension.begin(), extension.end(), extension.begin(), ::tolower); + std::transform(extensionToFind.begin(), extensionToFind.end(), extensionToFind.begin(), ::tolower); + + if(extension == extensionToFind) + return true; + else + return false; +} + +///See if the file has the "nif" extension. +bool isNIF(std::string filename) +{ + return hasExtension(filename,"nif"); +} +///See if the file has the "bsa" extension. +bool isBSA(std::string filename) +{ + return hasExtension(filename,"bsa"); +} + +///Check all the nif files in the given BSA archive +void readBSA(std::string filename) +{ + Bsa::BSAFile bsa; + bsa.open(filename.c_str()); + + const Bsa::BSAFile::FileList &files = bsa.getList(); + Bsa::addBSA(filename,"Bsa Files"); + + for(unsigned int i=0; i -#include -#include "../../mangle/stream/servers/file_stream.hpp" -#include "../node.hpp" -#include "../controller.hpp" -#include "../data.hpp" - -using namespace Mangle::Stream; -using namespace std; -using namespace Nif; - -// Display very verbose information -bool verbose = false; - -void doVector(const Vector *vec) -{ - cout << "[" - << vec->array[0] << "," - << vec->array[1] << "," - << vec->array[2] << "]\n"; -} - -void doVector4(const Vector4 *vec) -{ - cout << "[" - << vec->array[0] << "," - << vec->array[1] << "," - << vec->array[2] << "," - << vec->array[3] << "]\n"; -} - -void doMatrix(const Matrix *mat) -{ - cout << "Matrix:\n"; - for(int i=0; i<3; i++) - { - cout << " "; - doVector(&mat->v[i]); - } -} - -void doTrafo(const Transformation* trafo) -{ - cout << "--- transformation:\n"; - cout << "Pos: "; doVector(&trafo->pos); - cout << "Rot: "; doMatrix(&trafo->rotation); - cout << "Scale: " << trafo->scale << endl; - cout << "Vel: "; doVector(&trafo->velocity); - cout << "--- end transformation\n"; -} - -void doExtra(Extra *e) -{ - cout << "Extra: " << e->extra.getIndex() << endl; -} - -void doControlled(Controlled *c) -{ - doExtra(c); - cout << "Controller: " << c->controller.getIndex() << endl; -} - -void doNamed(Named *n) -{ - doControlled(n); - cout << "Name: " << n->name.toString() << endl; -} - -void doNode(Node *n) -{ - doNamed(n); - - cout << "Flags: 0x" << hex << n->flags << dec << endl; - doTrafo(n->trafo); - - cout << "Properties:"; - for(int i=0; iprops.length(); i++) - cout << " " << n->props.getIndex(i); - cout << endl; - - if(n->hasBounds) - { - cout << "Bounding box:\n"; - doVector(n->boundPos); - doMatrix(n->boundRot); - doVector(n->boundXYZ); - } - - if(n->boneTrafo) - { - cout << "This is a bone: "; - if(n->boneIndex == -1) - cout << "root bone\n"; - else - cout << "index " << n->boneIndex << endl; - } -} - -void doNiTriShape(NiTriShape *n) -{ - doNode(n); - - cout << "Shape data: " << n->data.getIndex() << endl; - cout << "Skin instance: " << n->skin.getIndex() << endl; -} - -void doNiSkinData(NiSkinData *n) -{ - int c = n->bones.size(); - - cout << "Global transformation:\n"; - doMatrix(&n->trafo->rotation); - doVector(&n->trafo->trans); - cout << "Scale: " << n->trafo->scale << endl; - - cout << "Bone number: " << c << endl; - for(int i=0; ibones[i]; - - cout << "-- Bone " << i << ":\n"; - doMatrix(&bi.trafo->rotation); - doVector(&bi.trafo->trans); - cout << "Scale: " << bi.trafo->scale << endl; - cout << "Unknown: "; doVector4(bi.unknown); - cout << "Weight number: " << bi.weights.length << endl; - - if(verbose) - for(int j=0; jdata.getIndex() << endl; - cout << "Root: " << n->root.getIndex() << endl; - cout << "Bones:"; - for(int i=0; ibones.length(); i++) - cout << " " << n->bones.getIndex(i); - cout << endl; -} - -void doNiNode(NiNode *n) -{ - doNode(n); - - cout << "Children:"; - for(int i=0; ichildren.length(); i++) - cout << " " << n->children.getIndex(i); - cout << endl; - - cout << "Effects:"; - for(int i=0; ieffects.length(); i++) - cout << " " << n->effects.getIndex(i); - cout << endl; -} - -void doNiStringExtraData(NiStringExtraData *s) -{ - doExtra(s); - cout << "String: " << s->string.toString() << endl; -} - -void doNiTextKeyExtraData(NiTextKeyExtraData *t) -{ - doExtra(t); - for(int i=0; ilist.size(); i++) - { - cout << "@time " << t->list[i].time << ":\n\"" - << t->list[i].text.toString() << "\"" << endl; - } -} - -void doController(Controller *r) -{ - cout << "Next controller: " << r->next.getIndex() << endl; - cout << "Flags: " << hex << r->flags << dec << endl; - cout << "Frequency: " << r->frequency << endl; - cout << "Phase: " << r->phase << endl; - cout << "Time start: " << r->timeStart << endl; - cout << "Time stop: " << r->timeStop << endl; - cout << "Target: " << r->target.getIndex() << endl; -} - -void doNiKeyframeController(NiKeyframeController *k) -{ - doController(k); - cout << "Data: " << k->data.getIndex() << endl; -} - -int main(int argc, char **args) -{ - if(argc != 2) - { - cout << "Specify a NIF file on the command line\n"; - return 1; - } - - StreamPtr file(new FileStream(args[1])); - NIFFile nif(file, args[1]); - - int num = nif.numRecords(); - - for(int i=0; irecName.toString() << endl; - - switch(r->recType) - { - case RC_NiNode: doNiNode((NiNode*)r); break; - case RC_NiSkinData: doNiSkinData((NiSkinData*)r); break; - case RC_NiSkinInstance: doNiSkinInstance((NiSkinInstance*)r); break; - case RC_NiTriShape: doNiTriShape((NiTriShape*)r); break; - case RC_NiStringExtraData: doNiStringExtraData((NiStringExtraData*)r); break; - case RC_NiSequenceStreamHelper: doNamed((Named*)r); break; - case RC_NiTextKeyExtraData: doNiTextKeyExtraData((NiTextKeyExtraData*)r); break; - case RC_NiKeyframeController: doNiKeyframeController((NiKeyframeController*)r); break; - } - - cout << endl; - } -} diff --git a/components/nif/tests/output/nif_bsa_test.out b/components/nif/tests/output/nif_bsa_test.out deleted file mode 100644 index 5499dafc7..000000000 --- a/components/nif/tests/output/nif_bsa_test.out +++ /dev/null @@ -1,5799 +0,0 @@ -Reading Morrowind.bsa -Decoding meshes\m\probe_journeyman_01.nif -Decoding meshes\b\b_n_redguard_f_skins.nif -Decoding meshes\b\b_n_redguard_m_skins.nif -Decoding meshes\b\b_n_redguard_f_wrist.nif -Decoding meshes\b\b_n_redguard_m_foot.nif -Decoding meshes\b\b_n_redguard_m_knee.nif -Decoding meshes\b\b_n_redguard_f_knee.nif -Decoding meshes\b\b_n_redguard_m_neck.nif -Decoding meshes\b\b_n_redguard_f_neck.nif -Decoding meshes\b\b_n_redguard_m_ankle.nif -Decoding meshes\b\b_n_redguard_f_ankle.nif -Decoding meshes\b\b_n_redguard_f_foot.nif -Decoding meshes\b\b_n_redguard_m_wrist.nif -Decoding meshes\b\b_n_redguard_f_groin.nif -Decoding meshes\b\b_n_redguard_m_groin.nif -Decoding meshes\b\b_n_redguard_m_head_02.nif -Decoding meshes\b\b_n_redguard_m_head_06.nif -Decoding meshes\b\b_n_redguard_m_head_04.nif -Decoding meshes\b\b_n_redguard_f_head_02.nif -Decoding meshes\b\b_n_redguard_f_head_06.nif -Decoding meshes\b\b_n_redguard_f_head_04.nif -Decoding meshes\b\b_n_redguard_m_hair_05.nif -Decoding meshes\b\b_n_redguard_m_hair_03.nif -Decoding meshes\b\b_n_redguard_m_hair_01.nif -Decoding meshes\b\b_n_redguard_f_hair_05.nif -Decoding meshes\b\b_n_redguard_f_hair_03.nif -Decoding meshes\b\b_n_redguard_f_hair_01.nif -Decoding meshes\b\b_n_redguard_m_forearm.nif -Decoding meshes\b\b_n_redguard_m_head_03.nif -Decoding meshes\b\b_n_redguard_m_head_01.nif -Decoding meshes\b\b_n_redguard_m_head_05.nif -Decoding meshes\b\b_n_redguard_f_head_03.nif -Decoding meshes\b\b_n_redguard_f_head_01.nif -Decoding meshes\b\b_n_redguard_f_head_05.nif -Decoding meshes\b\b_n_redguard_m_hair_06.nif -Decoding meshes\b\b_n_redguard_m_hair_04.nif -Decoding meshes\b\b_n_redguard_m_hair_02.nif -Decoding meshes\b\b_n_redguard_m_hair_00.nif -Decoding meshes\b\b_n_redguard_f_hair_04.nif -Decoding meshes\b\b_n_redguard_f_hair_02.nif -Decoding meshes\b\b_n_redguard_f_forearm.nif -Decoding meshes\b\b_n_redguard_f_upper leg.nif -Decoding meshes\b\b_n_redguard_f_upper arm.nif -Decoding meshes\b\b_n_redguard_m_upper leg.nif -Decoding meshes\b\b_n_redguard_m_hands.1st.nif -Decoding meshes\b\b_n_redguard_m_upper arm.nif -Decoding meshes\b\b_n_redguard_f_hands.1st.nif -Decoding meshes\w\w_longspear_daedric.nif -Decoding meshes\w\w_longsword_crystal.nif -Decoding meshes\w\w_longsword_daedric.nif -Decoding meshes\l\light_com_lantern_01.nif -Decoding meshes\l\light_com_candle_15.nif -Decoding meshes\l\light_com_candle_05.nif -Decoding meshes\l\light_com_candle_14.nif -Decoding meshes\l\light_com_candle_04.nif -Decoding meshes\l\light_com_candle_07.nif -Decoding meshes\l\light_com_candle_16.nif -Decoding meshes\l\light_com_candle_06.nif -Decoding meshes\l\light_com_candle_11.nif -Decoding meshes\l\light_com_candle_01.nif -Decoding meshes\l\light_com_candle_10.nif -Decoding meshes\l\light_com_candle_13.nif -Decoding meshes\l\light_com_candle_03.nif -Decoding meshes\l\light_com_candle_12.nif -Decoding meshes\l\light_com_candle_02.nif -Decoding meshes\l\light_com_candle_09.nif -Decoding meshes\l\light_com_candle_08.nif -Decoding meshes\l\light_com_sconce_02.nif -Decoding meshes\l\light_com_sconce_01.nif -Decoding meshes\l\light_com_lantern_02.nif -Decoding meshes\l\light_com_chandelier_03.nif -Decoding meshes\l\light_com_chandelier_01.nif -Decoding meshes\l\light_com_chandelier_05.nif -Decoding meshes\l\light_com_chandelier_02.nif -Decoding meshes\l\light_com_chandelier_06.nif -Decoding meshes\l\light_com_chandelier_04.nif -Decoding meshes\f\flora_ash_grass_b_01.nif -Decoding meshes\f\flora_ash_grass_r_01.nif -Decoding meshes\f\flora_ash_grass_w_01.nif -Decoding meshes\a\a_art_helm_bearclaw.nif -Decoding meshes\l\light_com_chandelier_01_l.nif -Decoding meshes\l\light_com_chandelier_05_l.nif -Decoding meshes\l\light_com_chandelier_03_l.nif -Decoding meshes\b\b_n_breton_f_head_04.nif -Decoding meshes\b\b_n_breton_f_hair_04.nif -Decoding meshes\b\b_n_breton_m_head_01.nif -Decoding meshes\b\b_n_breton_f_head_01.nif -Decoding meshes\b\b_n_breton_m_head_03.nif -Decoding meshes\b\b_n_breton_m_hair_01.nif -Decoding meshes\b\b_n_breton_m_head_04.nif -Decoding meshes\b\b_n_breton_f_head_05.nif -Decoding meshes\b\b_n_breton_m_head_07.nif -Decoding meshes\b\b_n_breton_m_head_08.nif -Decoding meshes\b\b_n_breton_m_head_06.nif -Decoding meshes\b\b_n_breton_m_forearm.nif -Decoding meshes\b\b_n_breton_m_head_05.nif -Decoding meshes\b\b_n_breton_f_head_06.nif -Decoding meshes\b\b_n_breton_f_hair_03.nif -Decoding meshes\b\b_n_breton_m_hair_04.nif -Decoding meshes\b\b_n_breton_m_hair_05.nif -Decoding meshes\b\b_n_breton_m_hair_03.nif -Decoding meshes\b\b_n_breton_f_head_03.nif -Decoding meshes\b\b_n_breton_f_hair_02.nif -Decoding meshes\b\b_n_breton_m_head_02.nif -Decoding meshes\b\b_n_breton_f_forearm.nif -Decoding meshes\b\b_n_breton_m_hair_02.nif -Decoding meshes\b\b_n_breton_f_head_02.nif -Decoding meshes\b\b_n_breton_m_hair_00.nif -Decoding meshes\b\b_n_breton_f_hair_05.nif -Decoding meshes\b\b_n_breton_f_hair_01.nif -Decoding meshes\b\b_n_breton_m_upper leg.nif -Decoding meshes\b\b_n_breton_f_upper leg.nif -Decoding meshes\b\b_n_breton_f_upper arm.nif -Decoding meshes\b\b_n_breton_m_upper arm.nif -Decoding meshes\b\b_n_breton_m_hand.1st.nif -Decoding meshes\b\b_n_breton_f_hands.1st.nif -Decoding meshes\a\a_gondolier_m_helmet.nif -Decoding meshes\m\light_com_candle_07.nif -Decoding meshes\m\misc_dwrv_ark_cube00.nif -Decoding meshes\m\misc_dwrv_artifact30.nif -Decoding meshes\m\misc_dwrv_artifact20.nif -Decoding meshes\m\misc_dwrv_artifact10.nif -Decoding meshes\m\misc_dwrv_artifact00.nif -Decoding meshes\m\misc_dwrv_artifact70.nif -Decoding meshes\m\misc_dwrv_artifact60.nif -Decoding meshes\m\misc_dwrv_artifact50.nif -Decoding meshes\m\misc_dwrv_artifact40.nif -Decoding meshes\m\misc_dwrv_artifact80.nif -Decoding meshes\m\misc_dwrv_pitcher00.nif -Decoding meshes\m\misc_dwrv_ark_key00.nif -Decoding meshes\i\in_dae_hall_l_corner.nif -Decoding meshes\i\in_dae_doorjamb_load.nif -Decoding meshes\i\in_dae_connect_lcave.nif -Decoding meshes\i\in_dae_platform_stairs.nif -Decoding meshes\i\in_dae_pillar_tall.max.nif -Decoding meshes\i\in_dae_platform_512_01.nif -Decoding meshes\i\in_dae_room_l_roof_01.nif -Decoding meshes\i\in_dae_room_l_floor_01.nif -Decoding meshes\i\in_dae_room_l_roof_02.nif -Decoding meshes\i\in_dae_room_l_side_01.nif -Decoding meshes\i\in_dae_room_l_roof_03.nif -Decoding meshes\i\in_dae_hall_ruin_l_02.nif -Decoding meshes\i\in_dae_hall_l_entry_01.nif -Decoding meshes\i\in_dae_hall_l_4way_02.nif -Decoding meshes\i\in_dae_hall_ruin_l_01.nif -Decoding meshes\i\in_dae_hall_l_3way_01.nif -Decoding meshes\i\in_dae_hall_l_4way_01.nif -Decoding meshes\i\in_dae_mezzanine_edge.nif -Decoding meshes\i\in_dae_room_ruin_roof_03.nif -Decoding meshes\i\in_dae_room_r_corner_02.nif -Decoding meshes\i\in_dae_room_l_corner_02.nif -Decoding meshes\i\in_dae_room_r_corner_01.nif -Decoding meshes\i\in_dae_room_l_corner_01.nif -Decoding meshes\i\in_dae_room_ruin_side_01.nif -Decoding meshes\i\in_dae_hall_l_endcap_01.nif -Decoding meshes\i\in_dae_hall_l_stairs_01.nif -Decoding meshes\i\in_com_trapbottom_01.nif -Decoding meshes\i\in_t_l_room_ceiling.nif -Decoding meshes\i\in_v_l_int_center_02.nif -Decoding meshes\i\in_v_l_int_corner_01.nif -Decoding meshes\i\in_v_l_int_stairs_04.nif -Decoding meshes\i\in_v_l_int_column_03.nif -Decoding meshes\i\in_v_l_int_column_01.nif -Decoding meshes\i\in_v_l_int_pillar_01.nif -Decoding meshes\i\in_v_l_int_bridge_02.nif -Decoding meshes\i\in_v_l_int_stairs_02.nif -Decoding meshes\i\in_v_l_int_stairs_03.nif -Decoding meshes\i\in_v_l_int_stairs_01.nif -Decoding meshes\i\in_v_l_int_lwall_01.nif -Decoding meshes\i\in_v_l_int_lwall_02.nif -Decoding meshes\i\in_v_l_int_center_01.nif -Decoding meshes\i\in_v_l_int_corner_03.nif -Decoding meshes\i\in_v_l_int_column_02.nif -Decoding meshes\i\in_v_l_int_bridge_01.nif -Decoding meshes\i\in_v_l_int_corner_02.nif -Decoding meshes\i\in_v_l_int_arches_01.nif -Decoding meshes\i\in_r_l_int_center_02.nif -Decoding meshes\i\in_r_l_int_corner_01.nif -Decoding meshes\i\in_r_l_int_column_01.nif -Decoding meshes\i\in_r_l_int_pillar_01.nif -Decoding meshes\i\in_r_l_int_bridge_02.nif -Decoding meshes\i\in_r_l_int_stairs_02.nif -Decoding meshes\i\in_r_l_short_ramp_01.nif -Decoding meshes\i\in_r_l_int_stairs_03.nif -Decoding meshes\i\in_r_l_int_stairs_01.nif -Decoding meshes\i\in_r_l_long_ramp_01.nif -Decoding meshes\i\in_r_l_int_lwall_01.nif -Decoding meshes\i\in_r_l_int_lwall_02.nif -Decoding meshes\i\in_r_l_int_center_01.nif -Decoding meshes\i\in_r_l_int_corner_03.nif -Decoding meshes\i\in_r_l_int_pillar_02.nif -Decoding meshes\i\in_r_l_int_bridge_01.nif -Decoding meshes\i\in_r_l_int_corner_02.nif -Decoding meshes\i\in_r_l_int_arches_01.nif -Decoding meshes\i\in_v_l_int_lcenter_02.nif -Decoding meshes\i\in_v_l_int_lcorner_01.nif -Decoding meshes\i\in_v_l_int_lcolumn_03.nif -Decoding meshes\i\in_v_l_int_lcolumn_01.nif -Decoding meshes\i\in_v_l_int_ceiling_01.nif -Decoding meshes\i\in_v_l_int_entrance_02.nif -Decoding meshes\i\in_v_l_int_lcorner_03.nif -Decoding meshes\i\in_v_l_int_lcenter_01.nif -Decoding meshes\i\in_v_l_int_lcolumn_02.nif -Decoding meshes\i\in_v_l_int_lcorner_02.nif -Decoding meshes\i\in_v_l_int_entrance_01.nif -Decoding meshes\i\in_r_l_int_lcenter_02.nif -Decoding meshes\i\in_r_l_int_lcorner_01.nif -Decoding meshes\i\in_r_l_int_lcolumn_01.nif -Decoding meshes\i\in_r_l_int_entrance_02.nif -Decoding meshes\i\in_r_l_int_lcorner_03.nif -Decoding meshes\i\in_r_l_int_lcenter_01.nif -Decoding meshes\i\in_r_l_int_lcenter_04.nif -Decoding meshes\i\in_r_l_int_lcenter_03.nif -Decoding meshes\i\in_r_l_int_balcony_01.nif -Decoding meshes\i\in_r_l_int_railing_01.nif -Decoding meshes\i\in_r_l_int_lcorner_02.nif -Decoding meshes\i\in_r_l_int_entrance_01.nif -Decoding meshes\i\in_r_l_int_entrance_03.nif -Decoding meshes\i\in_t_l_room_highentry.nif -Decoding meshes\m\misc_kwamaegg_gold_01.nif -Decoding meshes\i\in_v_l_int_lentrance_02.nif -Decoding meshes\i\in_v_l_int_partition_01.nif -Decoding meshes\i\in_v_l_int_lentrance_01.nif -Decoding meshes\i\in_r_l_int_lentrance_02.nif -Decoding meshes\i\in_r_l_int_partition_01.nif -Decoding meshes\i\in_r_l_int_lentrance_01.nif -Decoding meshes\i\in_r_l_int_lpartition_01.nif -Decoding meshes\i\cap01.nif -Decoding meshes\i\box02.nif -Decoding meshes\i\in_ar_s2.nif -Decoding meshes\i\in_ar_s3.nif -Decoding meshes\i\in_ar_s1.nif -Decoding meshes\i\in_ar_s6.nif -Decoding meshes\i\in_ar_s7.nif -Decoding meshes\i\in_ar_s4.nif -Decoding meshes\i\in_ar_s5.nif -Decoding meshes\i\in_ar_02.nif -Decoding meshes\i\in_ar_03.nif -Decoding meshes\i\in_ar_01.nif -Decoding meshes\i\in_ar_06.nif -Decoding meshes\i\in_ar_07.nif -Decoding meshes\i\in_ar_04.nif -Decoding meshes\i\in_ar_05.nif -Decoding meshes\i\in_ar_08.nif -Decoding meshes\i\in_ar_09.nif -Decoding meshes\i\in_ci_01.nif -Decoding meshes\i\in_ar_10.nif -Decoding meshes\i\in_imp_fireplace_grand.nif -Decoding meshes\i\in_t_s_plain_hall_01.nif -Decoding meshes\i\in_t_s_hallshaft_cap.nif -Decoding meshes\i\in_t_s_hall_ramp_01.nif -Decoding meshes\i\in_t_s_room_side_01.nif -Decoding meshes\i\in_v_s_int_center_02.nif -Decoding meshes\i\in_v_s_int_corner_01.nif -Decoding meshes\i\in_v_s_int_column_01.nif -Decoding meshes\i\in_v_s_int_lwall_01.nif -Decoding meshes\i\in_v_s_int_lwall_03.nif -Decoding meshes\i\in_v_s_int_lwall_02.nif -Decoding meshes\i\in_v_s_int_lwall_04.nif -Decoding meshes\i\in_v_s_int_center_01.nif -Decoding meshes\i\in_v_s_int_corner_03.nif -Decoding meshes\i\in_v_s_int_center_04.nif -Decoding meshes\i\in_v_s_int_center_03.nif -Decoding meshes\i\in_v_s_int_corner_02.nif -Decoding meshes\i\in_v_s_int_lrail_01.nif -Decoding meshes\i\in_r_s_int_center_02.nif -Decoding meshes\i\in_r_s_int_center_06.nif -Decoding meshes\i\in_r_s_int_corner_01.nif -Decoding meshes\i\in_r_s_int_stairs_04.nif -Decoding meshes\i\in_r_s_int_column_01.nif -Decoding meshes\i\in_r_s_int_pillar_01.nif -Decoding meshes\i\in_r_s_int_bridge_02.nif -Decoding meshes\i\in_r_s_int_ledge_03.nif -Decoding meshes\i\in_r_s_int_ledge_02.nif -Decoding meshes\i\in_r_s_int_ledge_01.nif -Decoding meshes\i\in_r_s_int_bridge_03.nif -Decoding meshes\i\in_r_s_int_stairs_05.nif -Decoding meshes\i\in_r_s_int_stairs_02.nif -Decoding meshes\i\in_r_s_int_center_05.nif -Decoding meshes\i\in_r_s_short_ramp_01.nif -Decoding meshes\i\in_r_s_int_stairs_03.nif -Decoding meshes\i\in_r_s_int_stairs_01.nif -Decoding meshes\i\in_r_s_long_ramp_01.nif -Decoding meshes\i\in_r_s_int_lwall_01.nif -Decoding meshes\i\in_r_s_int_lwall_02.nif -Decoding meshes\i\in_r_s_int_center_01.nif -Decoding meshes\i\in_r_s_int_corner_03.nif -Decoding meshes\i\in_r_s_int_center_04.nif -Decoding meshes\i\in_r_s_int_pillar_02.nif -Decoding meshes\i\in_r_s_int_center_03.nif -Decoding meshes\i\in_r_s_int_bridge_01.nif -Decoding meshes\i\in_r_s_int_corner_02.nif -Decoding meshes\i\in_r_s_int_steps_01.nif -Decoding meshes\i\in_r_s_int_steps_02.nif -Decoding meshes\i\in_r_s_int_steps_03.nif -Decoding meshes\i\in_r_s_int_stairs_06.nif -Decoding meshes\i\in_t_s_hall_endcap_01.nif -Decoding meshes\i\in_v_s_int_lcenter_02.nif -Decoding meshes\i\in_v_s_int_lstairs_04.nif -Decoding meshes\i\in_v_s_int_lcorner_01.nif -Decoding meshes\i\in_v_s_int_lcolumn_03.nif -Decoding meshes\i\in_v_s_int_lcolumn_01.nif -Decoding meshes\i\in_v_s_int_lbridge_02.nif -Decoding meshes\i\in_v_s_int_lpillar_01.nif -Decoding meshes\i\in_v_s_int_entrance_02.nif -Decoding meshes\i\in_v_s_int_lstairs_03.nif -Decoding meshes\i\in_v_s_int_lstairs_02.nif -Decoding meshes\i\in_v_s_int_lcorner_03.nif -Decoding meshes\i\in_v_s_int_lcenter_01.nif -Decoding meshes\i\in_v_s_int_lcenter_04.nif -Decoding meshes\i\in_v_s_int_lstairs_01.nif -Decoding meshes\i\in_v_s_int_lcolumn_02.nif -Decoding meshes\i\in_v_s_int_larches_01.nif -Decoding meshes\i\in_v_s_int_lcenter_03.nif -Decoding meshes\i\in_v_s_int_lcorner_02.nif -Decoding meshes\i\in_v_s_int_lbridge_01.nif -Decoding meshes\i\in_v_s_int_entrance_01.nif -Decoding meshes\i\in_v_s_lint_center_01.nif -Decoding meshes\i\in_r_s_int_lcenter_02.nif -Decoding meshes\i\in_r_s_int_lcenter_06.nif -Decoding meshes\i\in_r_s_int_lcorner_01.nif -Decoding meshes\i\in_r_s_int_lcolumn_01.nif -Decoding meshes\i\in_r_s_int_entrance_02.nif -Decoding meshes\i\in_r_s_int_lcenter_05.nif -Decoding meshes\i\in_r_s_int_lcorner_03.nif -Decoding meshes\i\in_r_s_int_lcenter_01.nif -Decoding meshes\i\in_r_s_int_lcenter_04.nif -Decoding meshes\i\in_r_s_int_larches_01.nif -Decoding meshes\i\in_r_s_int_lcenter_03.nif -Decoding meshes\i\in_r_s_int_balcony_01.nif -Decoding meshes\i\in_r_s_int_railing_01.nif -Decoding meshes\i\in_r_s_int_lcorner_02.nif -Decoding meshes\i\in_r_s_int_entrance_01.nif -Decoding meshes\i\in_t_s_pillar_large_01.nif -Decoding meshes\i\in_t_s_plain_hall_ramp.nif -Decoding meshes\i\in_t_s_plain_turret_02.nif -Decoding meshes\i\in_t_s_plain_shaft_cap.nif -Decoding meshes\i\in_t_s_plain_hall_4way.nif -Decoding meshes\i\in_t_s_plain_hall_3way.nif -Decoding meshes\i\in_t_s_plain_hall_plug.nif -Decoding meshes\i\in_t_s_shaft_elbow_01.nif -Decoding meshes\i\in_t_s_shaft_vconnect.nif -Decoding meshes\i\in_v_s_wall_column_01.nif -Decoding meshes\i\in_v_s_wall_column_02.nif -Decoding meshes\i\in_t_s_hall_small_corner.nif -Decoding meshes\i\in_t_s_plain_hall_corner.nif -Decoding meshes\i\in_t_s_plain_hall_endcap.nif -Decoding meshes\i\in_t_s_plain_room_center.nif -Decoding meshes\i\in_v_s_int_lentrance_02.nif -Decoding meshes\i\in_v_s_int_lentrance_04.nif -Decoding meshes\i\in_v_s_int_lentrance_06.nif -Decoding meshes\i\in_v_s_int_lentrance_01.nif -Decoding meshes\i\in_v_s_int_lentrance_03.nif -Decoding meshes\i\in_v_s_int_lentrance_05.nif -Decoding meshes\i\in_v_s_int_lpartition_01.nif -Decoding meshes\i\in_r_s_int_lentrance_02.nif -Decoding meshes\i\in_r_s_int_lentrance_04.nif -Decoding meshes\i\in_r_s_int_lentrance_06.nif -Decoding meshes\i\in_r_s_int_partition_01.nif -Decoding meshes\i\in_r_s_int_lentrance_01.nif -Decoding meshes\i\in_r_s_int_lentrance_03.nif -Decoding meshes\i\in_r_s_int_lentrance_05.nif -Decoding meshes\i\in_r_s_int_lplatform_01.nif -Decoding meshes\i\in_r_s_int_lpartition_01.nif -Decoding meshes\m\apparatus_a_spipe_01.nif -Decoding meshes\m\apparatus_s_alembic_01.nif -Decoding meshes\m\apparatus_g_alembic_01.nif -Decoding meshes\m\apparatus_a_alembic_01.nif -Decoding meshes\m\apparatus_m_alembic_01.nif -Decoding meshes\m\apparatus_j_alembic_01.nif -Decoding meshes\m\apparatus_s_retort_01.nif -Decoding meshes\m\apparatus_m_retort_01.nif -Decoding meshes\m\apparatus_j_retort_01.nif -Decoding meshes\m\apparatus_g_retort_01.nif -Decoding meshes\m\apparatus_a_retort_01.nif -Decoding meshes\i\in_dae_pillar_verytall.max.nif -Decoding meshes\i\in_dae_hall_l_staircurve2.nif -Decoding meshes\i\in_dae_hall_l_staircurve1.nif -Decoding meshes\i\in_dae_room_l_cornerout_01.nif -Decoding meshes\i\in_dae_room_ruin_cornerout.nif -Decoding meshes\i\scene root.nif -Decoding meshes\i\in_t_s_plain_hallshaft_cap.nif -Decoding meshes\m\apparatus_a_calcinator_01.nif -Decoding meshes\a\a_dragonscale_cuirass.nif -Decoding meshes\a\a_dragonscale_cuir_gnd.nif -Decoding meshes\l\light_ashl_lantern_01.nif -Decoding meshes\l\light_ashl_lantern_04.nif -Decoding meshes\l\light_ashl_lantern_05.nif -Decoding meshes\l\light_ashl_lantern_07.nif -Decoding meshes\l\light_ashl_lantern_03.nif -Decoding meshes\l\light_ashl_lantern_02.nif -Decoding meshes\l\light_ashl_lantern_06.nif -Decoding meshes\d\door_cavern_doors20.nif -Decoding meshes\d\door_cavern_doors10.nif -Decoding meshes\d\door_cavern_doors00.nif -Decoding meshes\m\misc_6th_ash_statue_01.nif -Decoding meshes\b\b_n_argonian_m_wrist.nif -Decoding meshes\b\b_n_argonian_m_skins.nif -Decoding meshes\b\b_n_argonian_f_skins.nif -Decoding meshes\b\b_n_argonian_f_neck.nif -Decoding meshes\b\b_n_argonian_m_neck.nif -Decoding meshes\b\b_n_argonian_f_wrist.nif -Decoding meshes\b\b_n_argonian_f_knee.nif -Decoding meshes\b\b_n_argonian_m_groin.nif -Decoding meshes\b\b_n_argonian_m_knee.nif -Decoding meshes\b\b_n_argonian_f_groin.nif -Decoding meshes\b\b_n_argonian_f_ankle.nif -Decoding meshes\b\b_n_argonian_m_ankle.nif -Decoding meshes\b\b_n_argonian_m_hair02.nif -Decoding meshes\b\b_n_argonian_f_hair02.nif -Decoding meshes\b\b_n_argonian_m_hair06.nif -Decoding meshes\b\b_n_argonian_m_head_02.nif -Decoding meshes\b\b_n_argonian_f_head_02.nif -Decoding meshes\b\b_n_argonian_f_hair03.nif -Decoding meshes\b\b_n_argonian_m_hair03.nif -Decoding meshes\b\b_n_argonian_m_forearm.nif -Decoding meshes\b\b_n_argonian_m_hair04.nif -Decoding meshes\b\b_n_argonian_f_hair04.nif -Decoding meshes\b\b_n_argonian_f_hair01.nif -Decoding meshes\b\b_n_argonian_m_hair01.nif -Decoding meshes\b\b_n_argonian_m_head_03.nif -Decoding meshes\b\b_n_argonian_m_head_01.nif -Decoding meshes\b\b_n_argonian_f_head_03.nif -Decoding meshes\b\b_n_argonian_f_head_01.nif -Decoding meshes\b\b_n_argonian_m_hair05.nif -Decoding meshes\b\b_n_argonian_f_hair05.nif -Decoding meshes\b\b_n_argonian_f_upper leg.nif -Decoding meshes\b\b_n_argonian_m_upper leg.nif -Decoding meshes\b\b_n_argonian_m_hands.1st.nif -Decoding meshes\b\b_n_argonian_f_hands.1st.nif -Decoding meshes\b\b_n_argonian_f_upper arm.nif -Decoding meshes\b\b_n_argonian_m_upper arm.nif -Decoding meshes\c\c_f_pants_g_common00.nif -Decoding meshes\c\c_f_pants_a_common00.nif -Decoding meshes\c\c_f_pants_k_common00.nif -Decoding meshes\c\c_f_pants_k_common02.nif -Decoding meshes\c\c_f_pants_a_common02.nif -Decoding meshes\c\c_f_pants_g_common02.nif -Decoding meshes\c\c_f_pants_k_common01.nif -Decoding meshes\c\c_f_pants_g_common01.nif -Decoding meshes\c\c_f_pants_a_common01.nif -Decoding meshes\c\c_f_pants_ul_common00.nif -Decoding meshes\c\c_f_pants_ul_common02.nif -Decoding meshes\c\c_f_pants_ul_common01.nif -Decoding meshes\c\c_f_pants_common_4_b_a.nif -Decoding meshes\c\c_f_pants_common_4_b_g.nif -Decoding meshes\c\c_f_pants_common_4_b_k.nif -Decoding meshes\c\c_f_pants_common_4_b_ul.nif -Decoding meshes\b\b_n_high elf_f_skins.nif -Decoding meshes\b\b_n_high elf_m_skins.nif -Decoding meshes\b\b_n_high elf_m_foot.nif -Decoding meshes\b\b_n_high elf_m_wrist.nif -Decoding meshes\b\b_n_high elf_m_knee.nif -Decoding meshes\b\b_n_high elf_f_knee.nif -Decoding meshes\b\b_n_high elf_f_groin.nif -Decoding meshes\b\b_n_high elf_m_groin.nif -Decoding meshes\b\b_n_high elf_m_neck.nif -Decoding meshes\b\b_n_high elf_f_neck.nif -Decoding meshes\b\b_n_high elf_f_wrist.nif -Decoding meshes\b\b_n_high elf_f_foot.nif -Decoding meshes\b\b_n_high elf_m_ankle.nif -Decoding meshes\b\b_n_high elf_f_ankle.nif -Decoding meshes\b\b_n_high elf_f_head_02.nif -Decoding meshes\b\b_n_high elf_f_head_06.nif -Decoding meshes\b\b_n_high elf_f_head_04.nif -Decoding meshes\b\b_n_high elf_m_head_02.nif -Decoding meshes\b\b_n_high elf_m_head_06.nif -Decoding meshes\b\b_n_high elf_m_head_04.nif -Decoding meshes\b\b_n_high elf_f_hair_01.nif -Decoding meshes\b\b_n_high elf_f_hair_03.nif -Decoding meshes\b\b_n_high elf_m_hair_05.nif -Decoding meshes\b\b_n_high elf_m_hair_01.nif -Decoding meshes\b\b_n_high elf_m_hair_03.nif -Decoding meshes\b\b_n_high elf_m_forearm.nif -Decoding meshes\b\b_n_high elf_f_head_03.nif -Decoding meshes\b\b_n_high elf_f_head_01.nif -Decoding meshes\b\b_n_high elf_f_head_05.nif -Decoding meshes\b\b_n_high elf_m_head_03.nif -Decoding meshes\b\b_n_high elf_m_head_01.nif -Decoding meshes\b\b_n_high elf_m_head_05.nif -Decoding meshes\b\b_n_high elf_f_hair_04.nif -Decoding meshes\b\b_n_high elf_f_hair_02.nif -Decoding meshes\b\b_n_high elf_m_hair_04.nif -Decoding meshes\b\b_n_high elf_m_hair_02.nif -Decoding meshes\b\b_n_high elf_f_forearm.nif -Decoding meshes\b\b_n_high elf_m_upper arm.nif -Decoding meshes\b\b_n_high elf_f_upper leg.nif -Decoding meshes\b\b_n_high elf_m_upper leg.nif -Decoding meshes\b\b_n_high elf_f_hands.1st.nif -Decoding meshes\b\b_n_high elf_m_hands.1st.nif -Decoding meshes\b\b_n_high elf_f_upper arm.nif -Decoding meshes\f\flora_bc_mushroom_08.nif -Decoding meshes\f\flora_bc_lilypad_02.nif -Decoding meshes\f\flora_bc_lilypad_03.nif -Decoding meshes\f\flora_bc_lilypad_01.nif -Decoding meshes\f\flora_bc_mushroom_05.nif -Decoding meshes\f\flora_bc_mushroom_01.nif -Decoding meshes\f\flora_bc_mushroom_02.nif -Decoding meshes\f\flora_bc_mushroom_03.nif -Decoding meshes\f\flora_bc_podplant_03.nif -Decoding meshes\f\flora_bc_podplant_02.nif -Decoding meshes\f\flora_bc_mushroom_07.nif -Decoding meshes\f\flora_bc_mushroom_06.nif -Decoding meshes\f\flora_bc_podplant_01.nif -Decoding meshes\f\flora_bc_mushroom_04.nif -Decoding meshes\f\flora_bc_shelffungus_02.nif -Decoding meshes\f\flora_bc_shelffungus_04.nif -Decoding meshes\f\flora_bc_shelffungus_03.nif -Decoding meshes\f\flora_bc_shelffungus_01.nif -Decoding meshes\m\misc_muck_shovel_01.nif -Decoding meshes\m\pick_secretmaster_01.nif -Decoding meshes\c\c_f_shirt_c_common01.nif -Decoding meshes\c\c_f_shirt_common_4_a_c.nif -Decoding meshes\c\c_f_shirt_common_4_c_c.nif -Decoding meshes\c\c_f_shirt_common_4_b_c.nif -Decoding meshes\c\c_f_shirt_c_commonl04.nif -Decoding meshes\c\c_f_shirt_c_commonl02.nif -Decoding meshes\c\c_f_skirt_g_common01.nif -Decoding meshes\c\c_f_skirt_g_common_4_c.nif -Decoding meshes\f\flora_emp_parasol_01.nif -Decoding meshes\f\flora_emp_parasol_02.nif -Decoding meshes\f\flora_emp_parasol_03.nif -Decoding meshes\c\c_m_robe_common_02r.nif -Decoding meshes\c\c_m_robe_common_02t.nif -Decoding meshes\c\c_m_robe_common_03a.nif -Decoding meshes\c\c_m_robe_common_05a.nif -Decoding meshes\c\c_m_robe_common_05c.nif -Decoding meshes\c\c_m_robe_common_03b.nif -Decoding meshes\c\c_m_robe_common_05b.nif -Decoding meshes\c\c_m_robe_common_02h.nif -Decoding meshes\c\c_m_robe_common_02tt.nif -Decoding meshes\c\c_m_robe_expensive_1.nif -Decoding meshes\c\c_m_robe_exquisite_1.nif -Decoding meshes\c\c_m_robe_common_02rr.nif -Decoding meshes\c\c_m_robe_extrav_1_c.nif -Decoding meshes\c\c_m_robe_common_02hh.nif -Decoding meshes\c\c_m_robe_expensive_2.nif -Decoding meshes\c\c_m_robe_common_02_gnd.nif -Decoding meshes\c\c_m_robe_common_01_gnd.nif -Decoding meshes\c\c_m_robe_extrav_2_gnd.nif -Decoding meshes\c\c_m_robe_expens_3.1st.nif -Decoding meshes\c\c_m_robe_common_4.1st.nif -Decoding meshes\c\c_m_robe_common_4_gnd.nif -Decoding meshes\c\c_m_robe_common_3_gnd.nif -Decoding meshes\c\c_m_robe_common_5.1st.nif -Decoding meshes\c\c_m_robe_common_02.1st.nif -Decoding meshes\c\c_m_robe_common_01.1st.nif -Decoding meshes\c\c_m_robe_expens_3_gnd.nif -Decoding meshes\c\c_m_robe_common_3.1st.nif -Decoding meshes\c\c_m_robe_extrav_1_gnd.nif -Decoding meshes\c\c_m_robe_extrav_2.1st.nif -Decoding meshes\c\c_m_robe_extrav_1r_gnd.nif -Decoding meshes\c\c_m_robe_extrav_1t_gnd.nif -Decoding meshes\c\c_m_robe_extrav_1h_gnd.nif -Decoding meshes\c\c_m_robe_extrav_1a_gnd.nif -Decoding meshes\c\c_m_robe_extrav_1b_gnd.nif -Decoding meshes\c\c_m_robe_extrav_1c_gnd.nif -Decoding meshes\c\c_m_robe_common_5_gnd.nif -Decoding meshes\c\c_m_robe_extrav_1t.1st.nif -Decoding meshes\c\c_m_robe_extrav_1r.1st.nif -Decoding meshes\c\c_m_robe_extrav_1a.1st.nif -Decoding meshes\c\c_m_robe_extrav_1b.1st.nif -Decoding meshes\c\c_m_robe_extrav_1c.1st.nif -Decoding meshes\c\c_m_robe_extrav_1h.1st.nif -Decoding meshes\c\c_m_robe_expensive_2a.nif -Decoding meshes\c\c_m_robe_extrav_1.1st.nif -Decoding meshes\c\c_m_robe_exquisite_1.1st.nif -Decoding meshes\c\c_m_robe_common_02hh_gnd.nif -Decoding meshes\c\c_m_robe_common_02tt.1st.nif -Decoding meshes\c\c_m_robe_common_02rr.1st.nif -Decoding meshes\c\c_m_robe_common_02hh.1st.nif -Decoding meshes\c\c_m_robe_exquisite_1_gnd.nif -Decoding meshes\c\c_m_robe_common_05a.1st.nif -Decoding meshes\c\c_m_robe_common_05b.1st.nif -Decoding meshes\c\c_m_robe_common_05c.1st.nif -Decoding meshes\c\c_m_robe_extrav_1_c.1st.nif -Decoding meshes\c\c_m_robe_common_03b_gnd.nif -Decoding meshes\c\c_m_robe_common_03a_gnd.nif -Decoding meshes\c\c_m_robe_expensive_1_gnd.nif -Decoding meshes\c\c_m_robe_common_05a_gnd.nif -Decoding meshes\c\c_m_robe_common_05b_gnd.nif -Decoding meshes\c\c_m_robe_common_05c_gnd.nif -Decoding meshes\c\c_m_robe_common_02tt_gnd.nif -Decoding meshes\c\c_m_robe_common_03b.1st.nif -Decoding meshes\c\c_m_robe_common_03a.1st.nif -Decoding meshes\c\c_m_robe_expensive_1.1st.nif -Decoding meshes\c\c_m_robe_expensive_2.1st.nif -Decoding meshes\c\c_m_robe_expensive_2_gnd.nif -Decoding meshes\c\c_m_robe_common_02h.1st.nif -Decoding meshes\c\c_m_robe_common_02t.1st.nif -Decoding meshes\c\c_m_robe_common_02r.1st.nif -Decoding meshes\c\c_m_robe_common_02rr_gnd.nif -Decoding meshes\c\c_m_robe_common_02h_gnd.nif -Decoding meshes\c\c_m_robe_common_02r_gnd.nif -Decoding meshes\c\c_m_robe_common_02t_gnd.nif -Decoding meshes\m\misc_argonianhead_01.nif -Decoding meshes\lavasteam.nif -Decoding meshes\left_arrow.nif -Decoding meshes\f\furn_ex_ashl_guarskin.nif -Decoding meshes\l\light_fire.nif -Decoding meshes\lower_arrow.nif -Decoding meshes\lava_sparks.nif -Decoding meshes\c\c_m_robe_expensive_2a_gnd.nif -Decoding meshes\c\c_m_robe_expensive_2a.1st.nif -Decoding meshes\b\b_n_khajiit_m_ankle.nif -Decoding meshes\b\b_n_khajiit_f_ankle.nif -Decoding meshes\b\b_n_khajiit_m_hair02.nif -Decoding meshes\b\b_n_khajiit_f_hair02.nif -Decoding meshes\b\b_n_khajiit_m_hair04.nif -Decoding meshes\b\b_n_khajiit_f_hair04.nif -Decoding meshes\b\b_n_khajiit_f_groin.nif -Decoding meshes\b\b_n_khajiit_m_groin.nif -Decoding meshes\b\b_n_khajiit_m_wrist.nif -Decoding meshes\b\b_n_khajiit_f_wrist.nif -Decoding meshes\b\b_n_khajiit_f_skins.nif -Decoding meshes\b\b_n_khajiit_m_skins.nif -Decoding meshes\b\b_n_khajiit_f_hair03.nif -Decoding meshes\b\b_n_khajiit_m_hair03.nif -Decoding meshes\b\b_n_khajiit_m_hair05.nif -Decoding meshes\b\b_n_khajiit_f_hair05.nif -Decoding meshes\b\b_n_khajiit_f_hair01.nif -Decoding meshes\b\b_n_khajiit_m_hair01.nif -Decoding meshes\b\b_n_khajiit_f_forearm.nif -Decoding meshes\b\b_n_khajiit_f_head_04.nif -Decoding meshes\b\b_n_khajiit_m_head_01.nif -Decoding meshes\b\b_n_khajiit_m_head_03.nif -Decoding meshes\b\b_n_khajiit_m_forearm.nif -Decoding meshes\b\b_n_khajiit_m_head_04.nif -Decoding meshes\b\b_n_khajiit_f_head_03.nif -Decoding meshes\b\b_n_khajiit_f_head_02.nif -Decoding meshes\b\b_n_khajiit_m_head_02.nif -Decoding meshes\b\b_n_khajiit_f_head_01.nif -Decoding meshes\b\b_n_khajiit_f_upper arm.nif -Decoding meshes\b\b_n_khajiit_m_upper arm.nif -Decoding meshes\b\b_n_khajiit_m_upper leg.nif -Decoding meshes\b\b_n_khajiit_f_upper leg.nif -Decoding meshes\b\b_n_khajiit_f_hands.1st.nif -Decoding meshes\b\b_n_khajiit_m_hands.1st.nif -Decoding meshes\o\flora_marshmerrow_02.nif -Decoding meshes\o\flora_marshmerrow_03.nif -Decoding meshes\o\flora_marshmerrow_01.nif -Decoding meshes\f\furn_pycave_spout00.nif -Decoding meshes\m\gold_100.nif -Decoding meshes\m\gold_025.nif -Decoding meshes\m\gold_010.nif -Decoding meshes\m\gold_001.nif -Decoding meshes\m\gold_005.nif -Decoding meshes\menu_help.nif -Decoding meshes\menu_main.nif -Decoding meshes\menu_book.nif -Decoding meshes\marker_light.nif -Decoding meshes\marker_north.nif -Decoding meshes\marker_error.nif -Decoding meshes\marker_arrow.nif -Decoding meshes\m\misc_quill.nif -Decoding meshes\menu_scroll.nif -Decoding meshes\menu_target.nif -Decoding meshes\a\a_bonemold_cuirass_c.nif -Decoding meshes\a\a_bonemold_bracer_w.nif -Decoding meshes\a\a_bonemold_boot_gnd.nif -Decoding meshes\a\a_bonemold_greaves_g.nif -Decoding meshes\a\a_bonemold_greaves_k.nif -Decoding meshes\a\a_bonemold_pauldron_fa.nif -Decoding meshes\a\a_bonemold_pauldron_ua.nif -Decoding meshes\a\a_bonemold_cuirass_gnd.nif -Decoding meshes\a\a_bonemold_gah_julan_c.nif -Decoding meshes\a\a_bonemold_armun_an_ua.nif -Decoding meshes\a\a_bonemold_greaves_gnd.nif -Decoding meshes\a\a_bonemold_bracer_gnd.nif -Decoding meshes\a\a_bonemold_armun_an_cl.nif -Decoding meshes\a\a_bonemold_greaves_ul.nif -Decoding meshes\a\a_bonemold_gah_julan_h.nif -Decoding meshes\a\a_bonemold_gah_j_ua_gnd.nif -Decoding meshes\a\a_bonemold_pauldron_gnd.nif -Decoding meshes\a\a_bonemold_armun_an_helm.nif -Decoding meshes\a\a_bonemold_gah_julan_cl.nif -Decoding meshes\a\a_bonemold_gah_julan_ua.nif -Decoding meshes\a\a_bonemold_armun_ua_gnd.nif -Decoding meshes\a\a_bonemold_chuzei_helmet.nif -Decoding meshes\d\door_redoran_tower_01.nif -Decoding meshes\a\a_bonemold_gah_julan_cgnd.nif -Decoding meshes\c\c_m_bracer_w_leather01.nif -Decoding meshes\b\b_n_orc_m_upper leg.nif -Decoding meshes\b\b_n_orc_f_upper leg.nif -Decoding meshes\b\b_n_orc_m_hands.1st.nif -Decoding meshes\b\b_n_orc_f_hands.1st.nif -Decoding meshes\b\b_n_orc_f_upper arm.nif -Decoding meshes\b\b_n_orc_m_upper arm.nif -Decoding meshes\c\c_m_bracer_w_clothwrap02.nif -Decoding meshes\l\light_de_lantern_08.nif -Decoding meshes\l\light_de_lantern_09.nif -Decoding meshes\l\light_de_lantern_04.nif -Decoding meshes\l\light_de_lantern_14.nif -Decoding meshes\l\light_de_lantern_05.nif -Decoding meshes\l\light_de_lantern_06.nif -Decoding meshes\l\light_de_lantern_07.nif -Decoding meshes\l\light_de_lantern_10.nif -Decoding meshes\l\light_de_lantern_01.nif -Decoding meshes\l\light_de_lantern_11.nif -Decoding meshes\l\light_de_lantern_02.nif -Decoding meshes\l\light_de_lantern_12.nif -Decoding meshes\l\light_de_lantern_03.nif -Decoding meshes\l\light_de_lantern_13.nif -Decoding meshes\l\light_dae_brazier00.nif -Decoding meshes\l\light_dwrv_neonbroke00.nif -Decoding meshes\l\light_de_candle_red_01.nif -Decoding meshes\l\light_de_candle_red_02.nif -Decoding meshes\l\light_de_candle_green_01.nif -Decoding meshes\l\light_de_streetlight_01.nif -Decoding meshes\l\light_de_candle_blue_01.nif -Decoding meshes\l\light_de_candle_ivory_01.nif -Decoding meshes\l\light_de_candle_blue_02.nif -Decoding meshes\o\contain_de_closet_02.nif -Decoding meshes\o\contain_com_chest_01.nif -Decoding meshes\o\contain_egg_kwama00.nif -Decoding meshes\o\contain_tramaroot_05.nif -Decoding meshes\o\contain_tramaroot_02.nif -Decoding meshes\o\contain_pot_blue_02.nif -Decoding meshes\o\contain_pot_blue_01.nif -Decoding meshes\o\contain_tramaroot_03.nif -Decoding meshes\o\contain_com_chest_02.nif -Decoding meshes\o\contain_ropecage_01.nif -Decoding meshes\o\contain_com_hutch_01.nif -Decoding meshes\o\contain_dwrv_desk00.nif -Decoding meshes\o\contain_de_closet_01.nif -Decoding meshes\o\contain_de_table_02.nif -Decoding meshes\o\contain_de_table_01.nif -Decoding meshes\o\contain_tramaroot_06.nif -Decoding meshes\o\contain_com_sack_02.nif -Decoding meshes\o\contain_com_sack_03.nif -Decoding meshes\o\contain_com_sack_01.nif -Decoding meshes\o\contain_tramaroot_01.nif -Decoding meshes\o\contain_dwrv_table00.nif -Decoding meshes\o\contain_de_chest_02.nif -Decoding meshes\o\contain_de_chest_01.nif -Decoding meshes\o\contain_tramaroot_04.nif -Decoding meshes\o\contain_dwrv_chest10.nif -Decoding meshes\o\contain_dwrv_chest00.nif -Decoding meshes\o\contain_dwrv_drawers00.nif -Decoding meshes\o\contain_de_drawers_01.nif -Decoding meshes\o\contain_rock_ebony_03.nif -Decoding meshes\o\contain_cavern_spore00.nif -Decoding meshes\o\contain_pot_redware_01.nif -Decoding meshes\o\contain_rock_glass_03.nif -Decoding meshes\o\contain_rock_glass_04.nif -Decoding meshes\o\contain_ropesphere_01.nif -Decoding meshes\o\contain_trama_shrub_06.nif -Decoding meshes\o\contain_trama_shrub_04.nif -Decoding meshes\o\contain_trama_shrub_02.nif -Decoding meshes\o\contain_rock_glass_01.nif -Decoding meshes\o\contain_rock_ebony_04.nif -Decoding meshes\o\contain_rock_ebony_07.nif -Decoding meshes\o\contain_rock_ebony_06.nif -Decoding meshes\o\contain_com_basket_01.nif -Decoding meshes\o\contain_com_closet_01.nif -Decoding meshes\o\contain_de_crate_logo.nif -Decoding meshes\o\contain_rock_glass_05.nif -Decoding meshes\o\contain_rock_ebony_05.nif -Decoding meshes\o\contain_pot_mottled_01.nif -Decoding meshes\o\contain_chest_small_02.nif -Decoding meshes\o\contain_rock_ebony_02.nif -Decoding meshes\o\contain_rock_glass_07.nif -Decoding meshes\o\contain_rock_ebony_01.nif -Decoding meshes\o\contain_dwrv_closet00.nif -Decoding meshes\o\contain_trama_shrub_05.nif -Decoding meshes\o\contain_trama_shrub_03.nif -Decoding meshes\o\contain_trama_shrub_01.nif -Decoding meshes\o\contain_chest_large_01.nif -Decoding meshes\o\contain_com_drawers_01.nif -Decoding meshes\o\contain_de_drawers_02.nif -Decoding meshes\o\contain_dwrv_barrel10.nif -Decoding meshes\o\contain_dwrv_barrel00.nif -Decoding meshes\o\contain_rock_glass_06.nif -Decoding meshes\o\contain_ropesphere_02.nif -Decoding meshes\o\contain_rock_glass_02.nif -Decoding meshes\o\contain_chest_small_01.nif -Decoding meshes\o\contain_rock_diamond_01.nif -Decoding meshes\o\contain_rock_diamond_03.nif -Decoding meshes\o\contain_rock_diamond_05.nif -Decoding meshes\o\contain_rock_diamond_07.nif -Decoding meshes\o\contain_rock_diamond_02.nif -Decoding meshes\o\contain_rock_diamond_04.nif -Decoding meshes\o\contain_rock_diamond_06.nif -Decoding meshes\o\contain_com_cupboard_01.nif -Decoding meshes\o\lootbag.nif -Decoding meshes\x\furn_de_lightpost_01.nif -Decoding meshes\c\c_m_pants_extrav_2_g.nif -Decoding meshes\c\c_m_pants_extrav_1_g.nif -Decoding meshes\c\c_m_pants_extrav_1_k.nif -Decoding meshes\c\c_m_pants_common_3_k.nif -Decoding meshes\c\c_m_pants_common_5_k.nif -Decoding meshes\c\c_m_pants_g_common00.nif -Decoding meshes\c\c_m_pants_common_5_g.nif -Decoding meshes\c\c_m_pants_common_3_g.nif -Decoding meshes\c\c_m_pants_a_common00.nif -Decoding meshes\c\c_m_pants_k_common00.nif -Decoding meshes\c\c_m_pants_k_common02.nif -Decoding meshes\c\c_m_pants_a_common02.nif -Decoding meshes\c\c_m_pants_g_common02.nif -Decoding meshes\c\c_m_pants_k_common01.nif -Decoding meshes\c\c_m_pants_g_common01.nif -Decoding meshes\c\c_m_pants_a_common01.nif -Decoding meshes\c\c_m_pants_extrav_2_k.nif -Decoding meshes\c\c_m_pants_extrav_1_a.nif -Decoding meshes\c\c_m_pants_extrav_2_a.nif -Decoding meshes\c\c_m_pants_common_5_a.nif -Decoding meshes\c\c_m_pants_common_3_a.nif -Decoding meshes\c\c_m_pants_expens_3_a.nif -Decoding meshes\c\c_m_pants_common_3c_k.nif -Decoding meshes\c\c_m_pants_extrav_1_ul.nif -Decoding meshes\c\c_m_pants_extrav_2_ul.nif -Decoding meshes\c\c_m_pants_ul_common00.nif -Decoding meshes\c\c_m_pants_common_3_ul.nif -Decoding meshes\c\c_m_pants_common_5_ul.nif -Decoding meshes\c\c_m_pants_common_3c_g.nif -Decoding meshes\c\c_m_pants_common_3b_g.nif -Decoding meshes\c\c_m_pants_ul_common02.nif -Decoding meshes\c\c_m_pants_gnd_common02.nif -Decoding meshes\c\c_m_pants_gnd_common00.nif -Decoding meshes\c\c_m_pants_ul_common01.nif -Decoding meshes\c\c_m_pants_common_3b_a.nif -Decoding meshes\c\c_m_pants_common_3c_a.nif -Decoding meshes\c\c_m_pants_expens_1_e_g.nif -Decoding meshes\c\c_m_pants_expens_1_e_a.nif -Decoding meshes\c\c_m_pants_expens_1_e_k.nif -Decoding meshes\c\c_m_pants_expens_1_a_g.nif -Decoding meshes\c\c_m_pants_expens_1_a_a.nif -Decoding meshes\c\c_m_pants_expens_1_a_k.nif -Decoding meshes\c\c_m_pants_expens_1_z_g.nif -Decoding meshes\c\c_m_pants_expens_1_z_a.nif -Decoding meshes\c\c_m_pants_expens_1_z_k.nif -Decoding meshes\c\c_m_pants_common_1_z_k.nif -Decoding meshes\c\c_m_pants_common_1_z_a.nif -Decoding meshes\c\c_m_pants_common_1_z_g.nif -Decoding meshes\c\c_m_pants_common_1_u_k.nif -Decoding meshes\c\c_m_pants_common_1_u_a.nif -Decoding meshes\c\c_m_pants_common_1_u_g.nif -Decoding meshes\c\c_m_pants_common_1_e_k.nif -Decoding meshes\c\c_m_pants_common_1_e_a.nif -Decoding meshes\c\c_m_pants_common_1_e_g.nif -Decoding meshes\c\c_m_pants_common_1_a_k.nif -Decoding meshes\c\c_m_pants_common_1_a_a.nif -Decoding meshes\c\c_m_pants_common_1_a_g.nif -Decoding meshes\c\c_m_pants_common_4_b_a.nif -Decoding meshes\c\c_m_pants_common_4_b_g.nif -Decoding meshes\c\c_m_pants_common_4_b_k.nif -Decoding meshes\c\c_m_pants_common_3b_k.nif -Decoding meshes\c\c_m_pants_common_5_gnd.nif -Decoding meshes\c\c_m_pants_common_3_gnd.nif -Decoding meshes\c\c_m_pants_extrav_1_gnd.nif -Decoding meshes\c\c_m_pants_extrav_2_gnd.nif -Decoding meshes\c\c_m_pants_common_3c_ul.nif -Decoding meshes\c\c_m_pants_common_3b_ul.nif -Decoding meshes\c\c_m_pants_gnd_common01.nif -Decoding meshes\c\c_m_pants_exquisite_1g.nif -Decoding meshes\c\c_m_pants_expens_3_gnd.nif -Decoding meshes\c\c_m_pants_common_4_b_ul.nif -Decoding meshes\c\c_m_pants_expens_1_a_ul.nif -Decoding meshes\c\c_m_pants_expens_1_e_ul.nif -Decoding meshes\c\c_m_pants_expens_1_u_ul.nif -Decoding meshes\c\c_m_pants_expens_1_z_ul.nif -Decoding meshes\c\c_m_pants_expens_1_z_gnd.nif -Decoding meshes\c\c_m_pants_expensive_1_k.nif -Decoding meshes\c\c_m_pants_expensive_1_g.nif -Decoding meshes\c\c_m_pants_expensive_1_a.nif -Decoding meshes\c\c_m_pants_expensive_2_k.nif -Decoding meshes\c\c_m_pants_expensive_2_g.nif -Decoding meshes\c\c_m_pants_expensive_2_a.nif -Decoding meshes\c\c_m_pants_exquisite_1_ul.nif -Decoding meshes\c\c_m_pants_common_1_u_gnd.nif -Decoding meshes\c\c_m_pants_common_1_a_ul.nif -Decoding meshes\c\c_m_pants_common_1_e_ul.nif -Decoding meshes\c\c_m_pants_common_1_z_ul.nif -Decoding meshes\c\c_m_pants_common_1_u_ul.nif -Decoding meshes\c\c_m_pants_common_1_z_gnd.nif -Decoding meshes\c\c_m_pants_exquisite_1_k.nif -Decoding meshes\c\c_m_pants_exquisite_1_g.nif -Decoding meshes\c\c_m_pants_exquisite_1_a.nif -Decoding meshes\c\c_m_pants_expens_1_u_gnd.nif -Decoding meshes\c\c_m_pants_common_4_b_gnd.nif -Decoding meshes\c\c_m_pants_common_3c_gnd.nif -Decoding meshes\c\c_m_pants_common_3b_gnd.nif -Decoding meshes\c\c_m_pants_common_1_a_gnd.nif -Decoding meshes\c\c_m_pants_common_1_e_gnd.nif -Decoding meshes\c\c_m_pants_exqisite_1_gnd.nif -Decoding meshes\c\c_m_pants_expens_1_a_gnd.nif -Decoding meshes\c\c_m_pants_expensive_1_ul.nif -Decoding meshes\c\c_m_pants_expensive_2_ul.nif -Decoding meshes\c\c_m_pants_expens_1_e_gnd.nif -Decoding meshes\a\a_dwemer_cl_pauldron.nif -Decoding meshes\a\a_dwemer_pauldron_ua.nif -Decoding meshes\a\a_dwemer_pauldron_fa.nif -Decoding meshes\a\a_dwemer_greaves_gnd.nif -Decoding meshes\a\a_dwemer_greaves_ul.nif -Decoding meshes\a\a_dwemer_pauldron_gnd.nif -Decoding meshes\a\a_dwemer_bracer_w_gnd.nif -Decoding meshes\i\xin_dagoth_bridge00.nif -Decoding meshes\m\text_quarto_open_04.nif -Decoding meshes\m\text_quarto_open_01.nif -Decoding meshes\m\text_quarto_open_03.nif -Decoding meshes\m\text_quarto_open_02.nif -Decoding meshes\c\c_m_pants_expensive_1_u_ul.nif -Decoding meshes\c\c_m_pants_exquisite_1_gnd.nif -Decoding meshes\c\c_m_pants_expensive_1_u_k.nif -Decoding meshes\c\c_m_pants_expensive_1_u_a.nif -Decoding meshes\c\c_m_pants_expensive_1_u_g.nif -Decoding meshes\c\c_m_pants_expensive_1_gnd.nif -Decoding meshes\c\c_m_pants_expensive_2_gnd.nif -Decoding meshes\f\furn_6th_bellhammer.nif -Decoding meshes\f\furn_6th_tallbanner.nif -Decoding meshes\f\furn_6th_dagothsymbol.nif -Decoding meshes\f\furn_6th_corpus_plate_01.nif -Decoding meshes\w\w_silver_shortsword.nif -Decoding meshes\c\c_glove_common1.1st.nif -Decoding meshes\c\c_glove_balmolagmer.nif -Decoding meshes\c\c_glove_extravagant1.nif -Decoding meshes\c\c_glove_common1_gnd.nif -Decoding meshes\c\c_glove_expensive1.1st.nif -Decoding meshes\c\c_glove_moragtong.1st.nif -Decoding meshes\c\c_glove_moragtong_gnd.nif -Decoding meshes\c\c_glove_expensive1_gnd.nif -Decoding meshes\c\c_glove_balmolagmer_gnd.nif -Decoding meshes\c\c_glove_balmolagmer.1st.nif -Decoding meshes\c\c_glove_extravagant1_gnd.nif -Decoding meshes\c\c_glove_extravagant1.1st.nif -Decoding meshes\e\magic_cast_levitate.nif -Decoding meshes\f\xfurn_redoran_flag_01.nif -Decoding meshes\c\c_m_shoe_f_leather00.nif -Decoding meshes\c\c_m_shoe_f_leather01.nif -Decoding meshes\c\c_m_shoe_common01_gnd.nif -Decoding meshes\c\c_m_shoe_expensive_1_f.nif -Decoding meshes\c\c_m_shoe_common02_gnd.nif -Decoding meshes\c\c_m_shoe_expens_1_gnd.nif -Decoding meshes\w\w_warhammer_daedric.nif -Decoding meshes\a\a_art_apostle_boots_r.nif -Decoding meshes\ashcloud.nif -Decoding meshes\w\w_art_warhammer_crusher.nif -Decoding meshes\a\a_art_apostle_boots_gnd.nif -Decoding meshes\f\furn_mudcave_pool00.nif -Decoding meshes\f\furn_mudcave_spout00.nif -Decoding meshes\f\furn_cushion_round_07.nif -Decoding meshes\f\furn_cushion_round_03.nif -Decoding meshes\f\furn_cushion_round_04.nif -Decoding meshes\f\furn_cushion_square_03.nif -Decoding meshes\f\furn_cushion_square_01.nif -Decoding meshes\f\furn_cushion_square_07.nif -Decoding meshes\f\furn_cushion_square_05.nif -Decoding meshes\f\furn_cushion_square_09.nif -Decoding meshes\f\furn_cushion_round_02.nif -Decoding meshes\f\furn_cushion_round_01.nif -Decoding meshes\f\furn_cushion_round_06.nif -Decoding meshes\f\furn_cushion_square_02.nif -Decoding meshes\f\furn_cushion_square_06.nif -Decoding meshes\f\furn_cushion_square_04.nif -Decoding meshes\f\furn_cushion_square_08.nif -Decoding meshes\f\furn_cushion_round_05.nif -Decoding meshes\f\furn_rug_big_04_collision.nif -Decoding meshes\a\a_dreugh_cuirass_gnd.nif -Decoding meshes\o\flora_bittergreen_07.nif -Decoding meshes\o\flora_bittergreen_02.nif -Decoding meshes\o\flora_bc_podplant_04.nif -Decoding meshes\o\flora_bittergreen_03.nif -Decoding meshes\o\flora_bittergreen_08.nif -Decoding meshes\o\flora_bittergreen_09.nif -Decoding meshes\o\flora_bittergreen_04.nif -Decoding meshes\o\flora_bittergreen_06.nif -Decoding meshes\o\flora_bittergreen_01.nif -Decoding meshes\o\flora_bittergreen_10.nif -Decoding meshes\o\flora_bittergreen_05.nif -Decoding meshes\o\flora_black_lichen_02.nif -Decoding meshes\o\flora_black_anther_01.nif -Decoding meshes\o\flora_black_lichen_03.nif -Decoding meshes\o\flora_black_anther_02.nif -Decoding meshes\o\flora_black_lichen_01.nif -Decoding meshes\o\flora_bittergreen_pod_01.nif -Decoding meshes\button.nif -Decoding meshes\base_anim.nif -Decoding meshes\bloodsplat.nif -Decoding meshes\bloodsplat2.nif -Decoding meshes\bloodsplat3.nif -Decoding meshes\blightcloud.nif -Decoding meshes\base_animkna.nif -Decoding meshes\m\probe_apprentice_01.nif -Decoding meshes\c\c_shoes_extrav_2_gnd.nif -Decoding meshes\c\c_shoes_common_4_gnd.nif -Decoding meshes\c\c_shoes_common_3_gnd.nif -Decoding meshes\c\c_shoes_expensive_2.nif -Decoding meshes\c\c_shoes_extrav_1_gnd.nif -Decoding meshes\c\c_shoes_common_5_gnd.nif -Decoding meshes\c\c_shoes_expensive_03.nif -Decoding meshes\c\c_shoes_exquisite_1_f.nif -Decoding meshes\c\c_shoes_expensive_2_gnd.nif -Decoding meshes\c\c_shoes_expensive_3_gnd.nif -Decoding meshes\c\c_shoes_exquisite_1_gnd.nif -Decoding meshes\l\light_hangingl_blue_01.nif -Decoding meshes\l\light_hangings_blue_01.nif -Decoding meshes\x\flora_t_mushroom_02.nif -Decoding meshes\x\flora_t_mushroom_01.nif -Decoding meshes\x\flora_t_shelffungus_01.nif -Decoding meshes\b\b_n_wood elf_m_neck.nif -Decoding meshes\b\b_n_wood elf_f_neck.nif -Decoding meshes\b\b_n_wood elf_f_skins.nif -Decoding meshes\b\b_n_wood elf_m_skins.nif -Decoding meshes\b\b_n_wood elf_m_foot.nif -Decoding meshes\b\b_n_wood elf_m_wrist.nif -Decoding meshes\b\b_n_wood elf_m_knee.nif -Decoding meshes\b\b_n_wood elf_f_knee.nif -Decoding meshes\b\b_n_wood elf_f_groin.nif -Decoding meshes\b\b_n_wood elf_m_groin.nif -Decoding meshes\b\b_n_wood elf_f_wrist.nif -Decoding meshes\b\b_n_wood elf_f_foot.nif -Decoding meshes\b\b_n_wood elf_m_ankle.nif -Decoding meshes\b\b_n_wood elf_f_ankle.nif -Decoding meshes\b\b_n_wood elf_f_head_02.nif -Decoding meshes\b\b_n_wood elf_f_head_06.nif -Decoding meshes\b\b_n_wood elf_f_head_04.nif -Decoding meshes\b\b_n_wood elf_m_head_02.nif -Decoding meshes\b\b_n_wood elf_m_head_06.nif -Decoding meshes\b\b_n_wood elf_m_head_04.nif -Decoding meshes\b\b_n_wood elf_m_head_08.nif -Decoding meshes\b\b_n_wood elf_f_hair_05.nif -Decoding meshes\b\b_n_wood elf_f_hair_01.nif -Decoding meshes\b\b_n_wood elf_f_hair_03.nif -Decoding meshes\b\b_n_wood elf_m_hair_05.nif -Decoding meshes\b\b_n_wood elf_m_hair_01.nif -Decoding meshes\b\b_n_wood elf_m_hair_03.nif -Decoding meshes\b\b_n_wood elf_m_forearm.nif -Decoding meshes\b\b_n_wood elf_f_head_03.nif -Decoding meshes\b\b_n_wood elf_f_head_01.nif -Decoding meshes\b\b_n_wood elf_f_head_05.nif -Decoding meshes\b\b_n_wood elf_m_head_03.nif -Decoding meshes\b\b_n_wood elf_m_head_01.nif -Decoding meshes\b\b_n_wood elf_m_head_07.nif -Decoding meshes\b\b_n_wood elf_m_head_05.nif -Decoding meshes\b\b_n_wood elf_f_hair_04.nif -Decoding meshes\b\b_n_wood elf_f_hair_02.nif -Decoding meshes\b\b_n_wood elf_m_hair_04.nif -Decoding meshes\b\b_n_wood elf_m_hair_06.nif -Decoding meshes\b\b_n_wood elf_m_hair_02.nif -Decoding meshes\b\b_n_wood elf_f_forearm.nif -Decoding meshes\b\b_n_wood elf_m_upper arm.nif -Decoding meshes\b\b_n_wood elf_f_upper leg.nif -Decoding meshes\b\b_n_wood elf_m_upper leg.nif -Decoding meshes\b\b_n_wood elf_f_hands.1st.nif -Decoding meshes\b\b_n_wood elf_m_hands.1st.nif -Decoding meshes\b\b_n_wood elf_f_upper arm.nif -Decoding meshes\x\terraiin_rock_ma_07.nif -Decoding meshes\x\terraiin_rock_rm_07.nif -Decoding meshes\a\a_art_cuirass_lords_c.nif -Decoding meshes\a\a_art_cuirass_savior_c.nif -Decoding meshes\cursor.nif -Decoding meshes\cursormove.nif -Decoding meshes\a\a_art_cuirass_lords_gnd.nif -Decoding meshes\a\a_art_cuirass_savior_gnd.nif -Decoding meshes\f\furn_dwrv_fitting50.nif -Decoding meshes\f\furn_dwrv_fitting40.nif -Decoding meshes\f\furn_dwrv_fitting10.nif -Decoding meshes\f\furn_dwrv_fitting00.nif -Decoding meshes\f\furn_dwrv_fitting30.nif -Decoding meshes\f\furn_dwrv_fitting20.nif -Decoding meshes\f\furn_dwrv_cabinet00.nif -Decoding meshes\f\furn_dwrv_beltdrive00.nif -Decoding meshes\f\furn_dwrv_bookshelf00.nif -Decoding meshes\cursor_drop.nif -Decoding meshes\c\c_skirt_exquisite_1.nif -Decoding meshes\c\c_skirt_common_3_gnd.nif -Decoding meshes\c\c_skirt_common_2_gnd.nif -Decoding meshes\c\c_skirt_expensive_1.nif -Decoding meshes\c\c_skirt_expensive_3.nif -Decoding meshes\c\c_skirt_expensive_2.nif -Decoding meshes\c\c_skirt_common_5_gnd.nif -Decoding meshes\c\c_skirt_extravagant_1.nif -Decoding meshes\c\c_skirt_extravagant_2.nif -Decoding meshes\c\c_skirt_extravagant_1gnd.nif -Decoding meshes\c\c_skirt_extravagant_2gnd.nif -Decoding meshes\c\c_skirt_expensive_2_gnd.nif -Decoding meshes\c\c_skirt_expensive_3_gnd.nif -Decoding meshes\c\c_skirt_expensive_1_gnd.nif -Decoding meshes\c\c_skirt_exquisite_1_gnd.nif -Decoding meshes\w\magic_target_poison.nif -Decoding meshes\w\magic_target_conjure.nif -Decoding meshes\f\flora_muckspunge_06.nif -Decoding meshes\f\flora_muckspunge_07.nif -Decoding meshes\f\flora_muckspunge_04.nif -Decoding meshes\f\flora_muckspunge_05.nif -Decoding meshes\f\flora_muckspunge_02.nif -Decoding meshes\f\flora_muckspunge_03.nif -Decoding meshes\f\flora_muckspunge_01.nif -Decoding meshes\b\b_v_imperial_m_head_01.nif -Decoding meshes\b\b_v_imperial_f_head_01.nif -Decoding meshes\x\terrain_rock_ma_550.nif -Decoding meshes\x\terrain_lava_ventlg.nif -Decoding meshes\x\terrain_rock_ma_arch.nif -Decoding meshes\x\terrain_lava_ventlg01.nif -Decoding meshes\x\terrain_ashland_rock_16.nif -Decoding meshes\x\terrain_ashland_rock_14.nif -Decoding meshes\x\terrain_ashland_rock_12.nif -Decoding meshes\x\terrain_ashland_rock_10.nif -Decoding meshes\x\terrain_ashland_rock_08.nif -Decoding meshes\x\terrain_ashland_rock_04.nif -Decoding meshes\x\terrain_ashland_rock_06.nif -Decoding meshes\x\terrain_ashland_rock_02.nif -Decoding meshes\x\terrain_ashland_rock_15.nif -Decoding meshes\x\terrain_ashland_rock_13.nif -Decoding meshes\x\terrain_ashland_rock_11.nif -Decoding meshes\x\terrain_ashland_rock_09.nif -Decoding meshes\x\terrain_ashland_rock_05.nif -Decoding meshes\x\terrain_ashland_rock_07.nif -Decoding meshes\x\terrain_ashland_rock_01.nif -Decoding meshes\x\terrain_ashland_rock_03.nif -Decoding meshes\f\furn_spinningwheel_01.nif -Decoding meshes\f\xact_banner_hla_oad.nif -Decoding meshes\f\xact_banner_tel_fyr.nif -Decoding meshes\f\xact_banner_tel_mora.nif -Decoding meshes\f\xact_banner_tel_vos.nif -Decoding meshes\f\xact_banner_tel_aruhn.nif -Decoding meshes\f\xact_banner_gnaar_mok.nif -Decoding meshes\f\xact_banner_sadrith_mora.nif -Decoding meshes\f\xact_banner_ald_velothi.nif -Decoding meshes\f\xact_banner_tel_branora.nif -Decoding meshes\a\a_art_dragon_cuirass_c.nif -Decoding meshes\d\ex_dae_door_load_oval.nif -Decoding meshes\d\scene root.nif -Decoding meshes\c\c_skirt_extravagant_2_gnd.nif -Decoding meshes\c\c_skirt_extravagant_1_gnd.nif -Decoding meshes\m\probe_grandmaster_01.nif -Decoding meshes\a\a_m_chitin_hands.1st.nif -Decoding meshes\a\a_m_chitin_g_greaves.nif -Decoding meshes\a\a_m_chitin_boot_gnd.nif -Decoding meshes\a\a_m_chitin_ua_pauldron.nif -Decoding meshes\a\a_m_chitin_greaves_gnd.nif -Decoding meshes\a\a_m_chitin_ul_greaves.nif -Decoding meshes\a\a_m_chitin_cuirass_gnd.nif -Decoding meshes\a\a_m_chitin_pauldron_cl.nif -Decoding meshes\a\a_m_chitin_gauntlet_gnd.nif -Decoding meshes\a\a_art_ebon_cuirass_c.nif -Decoding meshes\a\a_art_ebon_cuirass_gnd.nif -Decoding meshes\w\w_art_staff_hasedoki.nif -Decoding meshes\e\blight.nif -Decoding meshes\e\hand01.nif -Decoding meshes\e\absorb.nif -Decoding meshes\e\cure_hit.nif -Decoding meshes\e\corprus.nif -Decoding meshes\w\shadowshortbladeonehand.nif -Decoding meshes\e\magic_hit.nif -Decoding meshes\e\magic_cast.nif -Decoding meshes\e\magic_area.nif -Decoding meshes\editormarker.nif -Decoding meshes\e\frost_hit.nif -Decoding meshes\b\b_v_dark elf_f_head_01.nif -Decoding meshes\b\b_v_dark elf_m_head_01.nif -Decoding meshes\a\a_nordicfur_skinned.nif -Decoding meshes\a\a_nordicfur_bracer_w.nif -Decoding meshes\a\a_nordicfur_boot_gnd.nif -Decoding meshes\a\a_nordicfur_greave_g.nif -Decoding meshes\a\a_nordicfur_greave_gnd.nif -Decoding meshes\a\a_nordicfur_hands.1st.nif -Decoding meshes\a\a_nordicfur_greave_ul.nif -Decoding meshes\a\a_nordicfur_pauldron_gnd.nif -Decoding meshes\a\a_nordicfur_gauntlet_gnd.nif -Decoding meshes\a\a_nordicfur_pauldron_cl.nif -Decoding meshes\a\a_nordicfur_pauldron_ua.nif -Decoding meshes\f\furn_practice_dummy.nif -Decoding meshes\a\a_art_fists_gauntlets.nif -Decoding meshes\fire_small.nif -Decoding meshes\f\ex_turf00.nif -Decoding meshes\f\furn_web00.nif -Decoding meshes\f\furn_web10.nif -Decoding meshes\f\furn_cot00.nif -Decoding meshes\a\a_art_fists_gauntlets.1st.nif -Decoding meshes\a\a_nordicfur_m_cuirass_gnd.nif -Decoding meshes\a\a_netch_m_greave_ul.nif -Decoding meshes\a\a_netch_m_greave_gnd.nif -Decoding meshes\a\a_netch_m_hands.1st.nif -Decoding meshes\a\a_netch_boiled_helm.nif -Decoding meshes\a\a_netch_m_pauldron_ua.nif -Decoding meshes\a\a_netch_m_pauldron_gnd.nif -Decoding meshes\a\a_netch_m_currais2_gnd.nif -Decoding meshes\a\a_netch_m_cuirass_gnd.nif -Decoding meshes\a\a_netch_m_pauldron_cl.nif -Decoding meshes\a\a_netch_m_gauntlet_gnd.nif -Decoding meshes\c\c_m_shirt_common_3_w.nif -Decoding meshes\c\c_m_shirt_common_5_w.nif -Decoding meshes\c\c_m_shirt_common_5_c.nif -Decoding meshes\c\c_m_shirt_common_3_c.nif -Decoding meshes\c\c_m_shirt_expens_3_w.nif -Decoding meshes\c\c_m_shirt_extrav_1_c.nif -Decoding meshes\c\c_m_shirt_extrav_2_c.nif -Decoding meshes\c\c_m_shirt_c_common01.nif -Decoding meshes\c\c_m_shirt_common_5_f.nif -Decoding meshes\c\c_m_shirt_extrav_1_w.nif -Decoding meshes\c\c_m_shirt_extrav_2_w.nif -Decoding meshes\c\c_m_shirt_c_common03.nif -Decoding meshes\c\c_m_shirt_w_common03.nif -Decoding meshes\c\c_m_shirt_expens_3_c.nif -Decoding meshes\c\c_m_shirt_extrav_1_tfa.nif -Decoding meshes\c\c_m_shirt_extrav_1_rfa.nif -Decoding meshes\c\c_m_shirt_extrav_1_hfa.nif -Decoding meshes\c\c_m_shirt_expens_1_z_f.nif -Decoding meshes\c\c_m_shirt_common_1_z_c.nif -Decoding meshes\c\c_m_shirt_common_1_u_w.nif -Decoding meshes\c\c_m_shirt_common_1_u_c.nif -Decoding meshes\c\c_m_shirt_common_2tt_w.nif -Decoding meshes\c\c_m_shirt_common_2tt_c.nif -Decoding meshes\c\c_m_shirt_common_2_t_w.nif -Decoding meshes\c\c_m_shirt_common_2_t_c.nif -Decoding meshes\c\c_m_shirt_common_2rr_w.nif -Decoding meshes\c\c_m_shirt_common_2rr_c.nif -Decoding meshes\c\c_m_shirt_common_2_r_w.nif -Decoding meshes\c\c_m_shirt_common_2_r_c.nif -Decoding meshes\c\c_m_shirt_common_2hh_w.nif -Decoding meshes\c\c_m_shirt_common_2hh_c.nif -Decoding meshes\c\c_m_shirt_common_2_h_w.nif -Decoding meshes\c\c_m_shirt_common_2_h_c.nif -Decoding meshes\c\c_m_shirt_common_1_e_w.nif -Decoding meshes\c\c_m_shirt_common_1_e_c.nif -Decoding meshes\c\c_m_shirt_common_4_a_w.nif -Decoding meshes\c\c_m_shirt_common_1_a_w.nif -Decoding meshes\c\c_m_shirt_common_4_a_c.nif -Decoding meshes\c\c_m_shirt_common_1_a_c.nif -Decoding meshes\c\c_m_shirt_common_4_c_w.nif -Decoding meshes\c\c_m_shirt_common_4_c_c.nif -Decoding meshes\c\c_m_shirt_common_4_b_w.nif -Decoding meshes\c\c_m_shirt_common_4_b_c.nif -Decoding meshes\c\c_m_shirt_gondalier_c.nif -Decoding meshes\c\c_m_shirt_common_3c_w.nif -Decoding meshes\c\c_m_shirt_extrav_2_ua.nif -Decoding meshes\c\c_m_shirt_extrav_1_ua.nif -Decoding meshes\c\c_m_shirt_extrav_2_fa.nif -Decoding meshes\c\c_m_shirt_extrav_1_fa.nif -Decoding meshes\c\c_m_shirt_extrav_1_tw.nif -Decoding meshes\c\c_m_shirt_fa_commonl02.nif -Decoding meshes\c\c_m_shirt_fa_commonl04.nif -Decoding meshes\c\c_m_shirt_ua_commonl02.nif -Decoding meshes\c\c_m_shirt_ua_commonl04.nif -Decoding meshes\c\c_m_shirt_common_3c_c.nif -Decoding meshes\c\c_m_shirt_common_3b_c.nif -Decoding meshes\c\c_m_shirt_common_3c_ua.nif -Decoding meshes\c\c_m_shirt_common_3b_ua.nif -Decoding meshes\c\c_m_shirt_extrav_1_rw.nif -Decoding meshes\c\c_m_shirt_ua_common01.nif -Decoding meshes\c\c_m_shirt_common_3c_fa.nif -Decoding meshes\c\c_m_shirt_expens_1_e_w.nif -Decoding meshes\c\c_m_shirt_expens_1_e_c.nif -Decoding meshes\c\c_m_shirt_expens_1_a_w.nif -Decoding meshes\c\c_m_shirt_expens_1_z_w.nif -Decoding meshes\c\c_m_shirt_expens_1_z_c.nif -Decoding meshes\c\c_m_shirt_common_1_u_f.nif -Decoding meshes\c\c_m_shirt_common_1_a_f.nif -Decoding meshes\c\c_m_shirt_common_5_gnd.nif -Decoding meshes\c\c_m_shirt_common_3_gnd.nif -Decoding meshes\c\c_m_shirt_extrav_1_hw.nif -Decoding meshes\c\c_m_shirt_common_3_ua.nif -Decoding meshes\c\c_m_shirt_common_5_ua.nif -Decoding meshes\c\c_m_shirt_common_3_fa.nif -Decoding meshes\c\c_m_shirt_extrav_1_hua.nif -Decoding meshes\c\c_m_shirt_expens_3_fa.nif -Decoding meshes\c\c_m_shirt_expens_3_ua.nif -Decoding meshes\c\c_m_shirt_extrav_1_rua.nif -Decoding meshes\c\c_m_shirt_w_commonl04.nif -Decoding meshes\c\c_m_shirt_c_commonl04.nif -Decoding meshes\c\c_m_shirt_extrav_1_gnd.nif -Decoding meshes\c\c_m_shirt_extrav_2_gnd.nif -Decoding meshes\c\c_m_shirt_extrav_1_tua.nif -Decoding meshes\c\c_m_shirt_ua_common03.nif -Decoding meshes\c\c_m_shirt_fa_common03.nif -Decoding meshes\c\c_m_shirt_gnd_common03.nif -Decoding meshes\c\c_m_shirt_gnd_common01.nif -Decoding meshes\c\c_m_shirt_extrav_1_hc.nif -Decoding meshes\c\c_m_shirt_extrav_1_tc.nif -Decoding meshes\c\c_m_shirt_extrav_1_rc.nif -Decoding meshes\c\c_m_shirt_expens_3_gnd.nif -Decoding meshes\c\c_m_shirt_c_commonl02.nif -Decoding meshes\c\c_m_shirt_expens_1_z_gnd.nif -Decoding meshes\c\c_m_shirt_expensive_1_c.nif -Decoding meshes\c\c_m_shirt_expensive_1_w.nif -Decoding meshes\c\c_m_shirt_expensive_2_c.nif -Decoding meshes\c\c_m_shirt_expensive_2_w.nif -Decoding meshes\c\c_m_shirt_exquisite_1_ua.nif -Decoding meshes\c\c_m_shirt_common_1_u_gnd.nif -Decoding meshes\c\c_m_shirt_common_1_z_gnd.nif -Decoding meshes\c\c_m_shirt_common_1_e_fa.nif -Decoding meshes\c\c_m_shirt_exquisite_1_w.nif -Decoding meshes\c\c_m_shirt_exquisite_1_c.nif -Decoding meshes\c\c_m_shirt_exquisite_1_fa.nif -Decoding meshes\c\c_m_shirt_common_2hh_gnd.nif -Decoding meshes\c\c_m_shirt_common_2_h_gnd.nif -Decoding meshes\c\c_m_shirt_expens_1_u_gnd.nif -Decoding meshes\c\c_m_shirt_gondolier_gnd.nif -Decoding meshes\c\c_m_shirt_common_4_c_gnd.nif -Decoding meshes\c\c_m_shirt_common_4_a_ua.nif -Decoding meshes\c\c_m_shirt_common_4_c_ua.nif -Decoding meshes\c\c_m_shirt_common_4_b_ua.nif -Decoding meshes\c\c_m_shirt_common_2tt_gnd.nif -Decoding meshes\c\c_m_shirt_common_2_t_gnd.nif -Decoding meshes\c\c_m_shirt_common_4_b_gnd.nif -Decoding meshes\c\c_m_shirt_expens_1_u_fa.nif -Decoding meshes\c\c_m_shirt_expens_1_e_fa.nif -Decoding meshes\c\c_m_shirt_expens_1_a_fa.nif -Decoding meshes\c\c_m_shirt_expens_1_a_ua.nif -Decoding meshes\c\c_m_shirt_expens_1_e_ua.nif -Decoding meshes\c\c_m_shirt_expens_1_u_ua.nif -Decoding meshes\c\c_m_shirt_expens_1_z_ua.nif -Decoding meshes\c\c_m_shirt_common_4_a_gnd.nif -Decoding meshes\c\c_m_shirt_common_3c_gnd.nif -Decoding meshes\c\c_m_shirt_common_3b_gnd.nif -Decoding meshes\c\c_m_shirt_common_1_a_gnd.nif -Decoding meshes\c\c_m_shirt_expensive_1_fa.nif -Decoding meshes\c\c_m_shirt_expensive_2_fa.nif -Decoding meshes\c\c_m_shirt_common_2_h_fa.nif -Decoding meshes\c\c_m_shirt_common_2_r_fa.nif -Decoding meshes\c\c_m_shirt_common_2_t_fa.nif -Decoding meshes\c\c_m_shirt_common_2rr_gnd.nif -Decoding meshes\c\c_m_shirt_common_2_r_gnd.nif -Decoding meshes\c\c_m_shirt_common_1_e_gnd.nif -Decoding meshes\c\c_m_shirt_gnd_commonl02.nif -Decoding meshes\c\c_m_shirt_gnd_commonl04.nif -Decoding meshes\c\c_m_shirt_common_1_a_ua.nif -Decoding meshes\c\c_m_shirt_common_1_e_ua.nif -Decoding meshes\c\c_m_shirt_common_1_z_ua.nif -Decoding meshes\c\c_m_shirt_common_1_u_ua.nif -Decoding meshes\c\c_m_shirt_common_2hh_fa.nif -Decoding meshes\c\c_m_shirt_extrav_1_hgnd.nif -Decoding meshes\c\c_m_shirt_common_2_t_ua.nif -Decoding meshes\c\c_m_shirt_common_2tt_ua.nif -Decoding meshes\c\c_m_shirt_expens_1_a_gnd.nif -Decoding meshes\c\c_m_shirt_common_2_r_ua.nif -Decoding meshes\c\c_m_shirt_common_2rr_ua.nif -Decoding meshes\c\c_m_shirt_common_2_h_ua.nif -Decoding meshes\c\c_m_shirt_common_2hh_ua.nif -Decoding meshes\c\c_m_shirt_common_4_a_fa.nif -Decoding meshes\c\c_m_shirt_common_4_b_fa.nif -Decoding meshes\c\c_m_shirt_common_4_c_fa.nif -Decoding meshes\c\c_m_shirt_expensive_1_ua.nif -Decoding meshes\c\c_m_shirt_expensive_2_ua.nif -Decoding meshes\c\c_m_shirt_expens_1_e_gnd.nif -Decoding meshes\c\c_m_shirt_common_2rr_fa.nif -Decoding meshes\c\c_m_shirt_extrav_1_tgnd.nif -Decoding meshes\c\c_m_shirt_common_2tt_fa.nif -Decoding meshes\c\c_m_shirt_extrav_1_rgnd.nif -Decoding meshes\c\c_m_skirt_imperial_gnd.nif -Decoding meshes\c\c_m_skirt_templar_gnd.nif -Decoding meshes\c\c_m_skirt_common_01_gnd.nif -Decoding meshes\o\flora_gold_kanet_02.nif -Decoding meshes\o\flora_gold_kanet_01.nif -Decoding meshes\o\flora_green_lichen_02.nif -Decoding meshes\o\flora_green_lichen_03.nif -Decoding meshes\o\flora_green_lichen_01.nif -Decoding meshes\a\a_art_gauntlet_fist_gnd.nif -Decoding meshes\x\furn_imp_rubble_ring.nif -Decoding meshes\f\furn_ashl_bugbowl_01.nif -Decoding meshes\f\furn_ashl_bugbowl_02.nif -Decoding meshes\f\furn_ashl_bugbowl_03.nif -Decoding meshes\f\furn_ashl_chimes_01.nif -Decoding meshes\c\c_m_skirt_common_04_c_gnd.nif -Decoding meshes\c\c_m_shirt_expensive_1_a_c.nif -Decoding meshes\c\c_m_shirt_expensive_1_u_fa.nif -Decoding meshes\c\c_m_shirt_expensive_1_u_ua.nif -Decoding meshes\c\c_m_shirt_exquisite_1_gnd.nif -Decoding meshes\c\c_m_shirt_expensive_1_u_c.nif -Decoding meshes\c\c_m_shirt_expensive_1_u_w.nif -Decoding meshes\c\c_m_shirt_expensive_1_gnd.nif -Decoding meshes\c\c_m_shirt_expensive_2_gnd.nif -Decoding meshes\l\light_spear_skull00.nif -Decoding meshes\c\c_indoril_m_ul_pants.nif -Decoding meshes\c\c_indoril_m_g_pants.nif -Decoding meshes\c\c_indoril_m_k_pants.nif -Decoding meshes\x\ex_imp_plaza_stairs.nif -Decoding meshes\x\ex_imp_guardtower_02.nif -Decoding meshes\x\ex_imp_foundation_01.nif -Decoding meshes\x\ex_imp_towerb_med_01.nif -Decoding meshes\x\ex_imp_towers_med_01.nif -Decoding meshes\x\ex_imp_wall_arch_01.nif -Decoding meshes\x\ex_imp_arrowslit_01.nif -Decoding meshes\x\ex_imp_govman_stair.nif -Decoding meshes\x\ex_imp_kdoorframe_01.nif -Decoding meshes\x\ex_imp_wall_tower_01.nif -Decoding meshes\x\ex_imp_towerb_top_01.nif -Decoding meshes\x\ex_imp_towers_top_01.nif -Decoding meshes\x\ex_imp_gov_arrowlit.nif -Decoding meshes\x\ex_imp_guardtower_01.nif -Decoding meshes\x\ex_imp_foundation_02.nif -Decoding meshes\x\ex_imp_dragonstatue.nif -Decoding meshes\x\ex_imp_towers_light_01.nif -Decoding meshes\x\ex_imp_towerb_cons_02.nif -Decoding meshes\x\ex_imp_towers_base_01.nif -Decoding meshes\x\ex_imp_towerb_dark_01.nif -Decoding meshes\x\ex_imp_towerb_base_01.nif -Decoding meshes\x\ex_imp_towerb_cons_01.nif -Decoding meshes\x\ex_imp_towers_dark_01.nif -Decoding meshes\x\ex_imp_towerb_light_01.nif -Decoding meshes\x\ex_imp_wall_stairs_02.nif -Decoding meshes\x\ex_imp_wall_stairs_03.nif -Decoding meshes\x\ex_imp_wall_corner_02.nif -Decoding meshes\x\ex_imp_wall_stairs_04.nif -Decoding meshes\x\ex_imp_wall_corner_01.nif -Decoding meshes\x\ex_imp_wall_stairs_01.nif -Decoding meshes\x\ex_imp_govmansion_gate.nif -Decoding meshes\x\ex_imp_govmansion_wing.nif -Decoding meshes\x\ex_imp_towerb_roof_pitch.nif -Decoding meshes\x\ex_imp_govmansion_donjon.nif -Decoding meshes\x\contain_tramaroot_06.nif -Decoding meshes\xyz_pole2.nif -Decoding meshes\x\ex_skiff.nif -Decoding meshes\x\ex_ar_01.nif -Decoding meshes\x\ex_gg_02.nif -Decoding meshes\x\ex_gg_03.nif -Decoding meshes\x\ex_gg_01.nif -Decoding meshes\xbase_anim.nif -Decoding meshes\x\ex_dae_ruin_02_skew.nif -Decoding meshes\x\ex_dae_mehrunesdagon.nif -Decoding meshes\x\ex_dae_b_bo_slpiece.nif -Decoding meshes\x\ex_dae_b_bo_bskirt1.nif -Decoding meshes\x\ex_dae_b_bo_bskirt2.nif -Decoding meshes\x\ex_dae_pillar_02_ruin.nif -Decoding meshes\x\ex_dae_ruin_entry.max.nif -Decoding meshes\x\ex_dae_ruin_04_skew_01.nif -Decoding meshes\x\ex_dae_ruin_04_skew_02.nif -Decoding meshes\x\ex_dae_ruin_02_skew_02.nif -Decoding meshes\x\ex_dae_b_bo_frontskirt.nif -Decoding meshes\x\ex_dae_boethiah_small.nif -Decoding meshes\x\ex_dae_malacath_small.nif -Decoding meshes\x\ex_dae_molagbal_small.nif -Decoding meshes\x\ex_dae_malacath_attack.nif -Decoding meshes\x\ex_dae_malacath_stand.nif -Decoding meshes\x\ex_dae_ruin_platform_01.nif -Decoding meshes\x\ex_dae_buttress_ruin_01.nif -Decoding meshes\x\ex_dae_sheogorath_small.nif -Decoding meshes\x\ex_dae_pillar_02_ruin_02.nif -Decoding meshes\x\ex_com_dframe_lthus.nif -Decoding meshes\x\ex_imp_govmansion_barbican.nif -Decoding meshes\x\ex_de_ship.nif -Decoding meshes\x\ex_de_oar.nif -Decoding meshes\x\ex_trellis.nif -Decoding meshes\x\ex_ruin_10.nif -Decoding meshes\x\ex_ruin_00.nif -Decoding meshes\x\ex_ruin_30.nif -Decoding meshes\x\ex_ruin_20.nif -Decoding meshes\x\ex_ruin_40.nif -Decoding meshes\x\ex_t_hook.nif -Decoding meshes\x\ex_dae_mehrunesdagon_small.nif -Decoding meshes\x\ex_dae_ruin_stair01_short.nif -Decoding meshes\w\w_dai-katana_daedric.nif -Decoding meshes\a\a_templar_m_w_bracer.nif -Decoding meshes\a\a_templar_greaves_k.nif -Decoding meshes\a\a_templar_greaves_g.nif -Decoding meshes\a\a_templar_m_boot_gnd.nif -Decoding meshes\a\a_templar_greaves_ul.nif -Decoding meshes\a\a_templar_pauldron_ua.nif -Decoding meshes\a\a_templar_pauldron_fa.nif -Decoding meshes\a\a_templar_greaves_gnd.nif -Decoding meshes\a\a_templar_m_cl_pauldron.nif -Decoding meshes\a\a_templar_m_cuirass_gnd.nif -Decoding meshes\a\a_masque_clavicus_vile.nif -Decoding meshes\w\w_battleaxe_daedric.nif -Decoding meshes\a\a_trollbone_cuirass_gnd.nif -Decoding meshes\f\furn_imp_rubble_ring.nif -Decoding meshes\f\furn_imp_stoneblock_01.nif -Decoding meshes\w\w_wakazashi_daedric.nif -Decoding meshes\a\a_imperial_greaves_g.nif -Decoding meshes\a\a_imperial_hands.1st.nif -Decoding meshes\a\a_imperial_greaves_k.nif -Decoding meshes\a\a_imperial_m_helmet.nif -Decoding meshes\a\a_imperial_a_boot01.nif -Decoding meshes\a\a_imperial_cl_pauldron.nif -Decoding meshes\a\a_imperial_ua_pauldron.nif -Decoding meshes\a\a_imperial_greaves_gnd.nif -Decoding meshes\a\a_imperial_greaves_ul.nif -Decoding meshes\a\a_imperial_a_boot_gnd.nif -Decoding meshes\a\a_imperial_m_cuirass_gnd.nif -Decoding meshes\a\a_imperial_pauldron_gnd.nif -Decoding meshes\a\a_indoril_m_boot_gnd.nif -Decoding meshes\a\a_indoril_m_fa_mail.nif -Decoding meshes\a\a_indoril_m_hands.1st.nif -Decoding meshes\a\a_indoril_m_cl_pauldron.nif -Decoding meshes\a\a_indoril_m_cuirass_gnd.nif -Decoding meshes\a\a_indoril_m_ua_pauldron.nif -Decoding meshes\a\a_indoril_m_pauldron_gnd.nif -Decoding meshes\a\a_indoril_m_gauntlet_gnd.nif -Decoding meshes\f\terrain_rocks_gl_03.nif -Decoding meshes\f\terrain_rocks_gl_02.nif -Decoding meshes\f\terrain_rocks_gl_01.nif -Decoding meshes\f\terrain_rocks_gl_04.nif -Decoding meshes\f\terrain_cairn_al_01.nif -Decoding meshes\f\terrain_cairn_al_02.nif -Decoding meshes\f\terrain_cairn_al_03.nif -Decoding meshes\f\terrain_rocks_wg_01.nif -Decoding meshes\f\terrain_rocks_wg_03.nif -Decoding meshes\f\terrain_rocks_wg_02.nif -Decoding meshes\f\terrain_rocks_wg_04.nif -Decoding meshes\f\terrain_cairn_ma_01.nif -Decoding meshes\f\terrain_cairn_ma_03.nif -Decoding meshes\f\terrain_cairn_ma_02.nif -Decoding meshes\f\terrain_rocks_ai_01.nif -Decoding meshes\f\terrain_rocks_ai_02.nif -Decoding meshes\f\terrain_rocks_ai_03.nif -Decoding meshes\f\terrain_rocks_ai_04.nif -Decoding meshes\w\w_art_longbow_shade.nif -Decoding meshes\f\furn_uni_weaponrack_01.nif -Decoding meshes\f\furn_uni_spearholder_01.nif -Decoding meshes\w\shadowlongbladeonehand.nif -Decoding meshes\w\shadowlongbladetwoclose.nif -Decoding meshes\f\furn_kneeling_stool_01.nif -Decoding meshes\m\artifact_devourer_01.nif -Decoding meshes\m\artifact_bittercup_01.nif -Decoding meshes\f\terrain_natural_bridge_01.nif -Decoding meshes\a\a_imperial_a_gauntlet_gnd.nif -Decoding meshes\c\c_templar_m_g_skirt.nif -Decoding meshes\l\light_paper_lantern_02.nif -Decoding meshes\l\light_paper_lantern_01.nif -Decoding meshes\l\light_paper_lantern_off.nif -Decoding meshes\f\flora_rm_scathecraw_02.nif -Decoding meshes\f\flora_rm_scathecraw_01.nif -Decoding meshes\b\b_v_wood elf_f_head_01.nif -Decoding meshes\b\b_v_wood elf_m_head_01.nif -Decoding meshes\m\misc_de_fishing_pole.nif -Decoding meshes\f\furn_pole_support_01.nif -Decoding meshes\w\w_art_molagbal_mace.nif -Decoding meshes\w\w_art_mehrunesrazor.nif -Decoding meshes\w\shadowmarksmancrossbow.nif -Decoding meshes\m\misc_redware_platter.nif -Decoding meshes\m\misc_redware_bowl_01.nif -Decoding meshes\m\misc_redware_pitcher.nif -Decoding meshes\f\furn_moldcave_pool00.nif -Decoding meshes\f\furn_moldcave_spout00.nif -Decoding meshes\f\furn_bonecave_pool00.nif -Decoding meshes\f\furn_bonecave_spout00.nif -Decoding meshes\f\furn_com_tapestry_02.nif -Decoding meshes\f\furn_com_cauldron_02.nif -Decoding meshes\f\furn_com_tapestry_03.nif -Decoding meshes\f\furn_com_tapestry_01.nif -Decoding meshes\f\furn_com_wincover_02.nif -Decoding meshes\f\furn_com_wincover_03.nif -Decoding meshes\f\furn_com_wincover_05.nif -Decoding meshes\f\furn_com_wincover_04.nif -Decoding meshes\f\furn_com_tapestry_05.nif -Decoding meshes\f\furn_com_cauldron_01.nif -Decoding meshes\f\furn_com_tapestry_04.nif -Decoding meshes\f\furn_com_bookshelf_01.nif -Decoding meshes\f\furn_com_coatofarms_01.nif -Decoding meshes\f\furn_com_torch_ring_02.nif -Decoding meshes\f\furn_com_coatofarms_02.nif -Decoding meshes\f\furn_com_torch_ring_01.nif -Decoding meshes\f\furn_com_bookshelf_02.nif -Decoding meshes\f\furn_com_lantern_hook.nif -Decoding meshes\f\furn_com_lantern_hook_02.nif -Decoding meshes\l\furn_de_firepit_f_01.nif -Decoding meshes\a\a_newtscale_cuirass.nif -Decoding meshes\b\b_n_imperial_m_knee.nif -Decoding meshes\b\b_n_imperial_f_knee.nif -Decoding meshes\b\b_n_imperial_f_wrist.nif -Decoding meshes\b\b_n_imperial_f_skins.nif -Decoding meshes\b\b_n_imperial_m_skins.nif -Decoding meshes\b\b_n_imperial_m_foot.nif -Decoding meshes\b\b_n_imperial_f_groin.nif -Decoding meshes\b\b_n_imperial_m_groin.nif -Decoding meshes\b\b_n_imperial_m_neck.nif -Decoding meshes\b\b_n_imperial_f_neck.nif -Decoding meshes\b\b_n_imperial_m_wrist.nif -Decoding meshes\b\b_n_imperial_m_ankle.nif -Decoding meshes\b\b_n_imperial_f_ankle.nif -Decoding meshes\b\b_n_imperial_f_foot.nif -Decoding meshes\b\b_n_imperial_m_hair_07.nif -Decoding meshes\b\b_n_imperial_m_hair_05.nif -Decoding meshes\b\b_n_imperial_m_hair_03.nif -Decoding meshes\b\b_n_imperial_m_hair_01.nif -Decoding meshes\b\b_n_imperial_m_hair_09.nif -Decoding meshes\b\b_n_imperial_f_hair_07.nif -Decoding meshes\b\b_n_imperial_f_hair_05.nif -Decoding meshes\b\b_n_imperial_f_hair_03.nif -Decoding meshes\b\b_n_imperial_f_hair_01.nif -Decoding meshes\b\b_n_imperial_m_head_02.nif -Decoding meshes\b\b_n_imperial_m_head_06.nif -Decoding meshes\b\b_n_imperial_m_head_04.nif -Decoding meshes\b\b_n_imperial_f_head_02.nif -Decoding meshes\b\b_n_imperial_f_head_06.nif -Decoding meshes\b\b_n_imperial_f_head_04.nif -Decoding meshes\b\b_n_imperial_m_forearm.nif -Decoding meshes\b\b_n_imperial_m_hair_06.nif -Decoding meshes\b\b_n_imperial_m_hair_04.nif -Decoding meshes\b\b_n_imperial_m_hair_02.nif -Decoding meshes\b\b_n_imperial_m_hair_00.nif -Decoding meshes\b\b_n_imperial_m_hair_08.nif -Decoding meshes\b\b_n_imperial_f_hair_06.nif -Decoding meshes\b\b_n_imperial_f_hair_04.nif -Decoding meshes\b\b_n_imperial_f_hair_02.nif -Decoding meshes\b\b_n_imperial_f_forearm.nif -Decoding meshes\b\b_n_imperial_m_head_03.nif -Decoding meshes\b\b_n_imperial_m_head_01.nif -Decoding meshes\b\b_n_imperial_m_head_07.nif -Decoding meshes\b\b_n_imperial_m_head_05.nif -Decoding meshes\b\b_n_imperial_f_head_03.nif -Decoding meshes\b\b_n_imperial_f_head_01.nif -Decoding meshes\b\b_n_imperial_f_head_07.nif -Decoding meshes\b\b_n_imperial_f_head_05.nif -Decoding meshes\a\a_m_imperialchain_gr_g.nif -Decoding meshes\a\a_m_imperialchain_pa_gnd.nif -Decoding meshes\a\a_m_imperialchain_c_gnd.nif -Decoding meshes\a\a_m_imperialchain_gr_ul.nif -Decoding meshes\a\a_m_imperialchain_helmet.nif -Decoding meshes\a\a_m_imperialchain_gr_gnd.nif -Decoding meshes\a\a_m_imperialchain_pa_ua.nif -Decoding meshes\b\b_n_imperial_f_upper leg.nif -Decoding meshes\b\b_n_imperial_m_upper leg.nif -Decoding meshes\b\b_n_imperial_m_hands.1st.nif -Decoding meshes\b\b_n_imperial_f_upper arm.nif -Decoding meshes\b\b_n_imperial_f_hands.1st.nif -Decoding meshes\b\b_n_imperial_m_upper arm.nif -Decoding meshes\m\pick_grandmaster_01.nif -Decoding meshes\d\door_dwrv_loaddown00.nif -Decoding meshes\f\furn_shrine_rilms_01.nif -Decoding meshes\f\furn_shrine_relms_01.nif -Decoding meshes\f\furn_shrine_felms_01.nif -Decoding meshes\f\furn_shrine_seryn_01.nif -Decoding meshes\f\furn_shrine_delyn_01.nif -Decoding meshes\f\furn_shrine_meris_01.nif -Decoding meshes\f\furn_shrine_roris_01.nif -Decoding meshes\f\furn_shrine_olms_01.nif -Decoding meshes\f\furn_shrine_vivec_01.nif -Decoding meshes\f\furn_shrine_llothis_01.nif -Decoding meshes\f\furn_shrine_aralor_01.nif -Decoding meshes\f\furn_shrine_nerevar_01.nif -Decoding meshes\f\furn_shrine_veloth_01.nif -Decoding meshes\f\furn_shrine_tribunal_01.nif -Decoding meshes\f\act_banner_tel_aruhn.nif -Decoding meshes\f\act_banner_gnaar_mok.nif -Decoding meshes\f\act_banner_tel_mora.nif -Decoding meshes\f\act_banner_ald_velothi.nif -Decoding meshes\f\act_banner_tel_branora.nif -Decoding meshes\f\act_banner_sadrith_mora.nif -Decoding meshes\a\a_m_imperialchain_cuirass.nif -Decoding meshes\l\light_velothismall_01.nif -Decoding meshes\l\light_velothi_brazier.nif -Decoding meshes\a\a_orcish_pauldron_ua.nif -Decoding meshes\a\a_orcish_pauldron_fa.nif -Decoding meshes\a\a_orcish_greaves_gnd.nif -Decoding meshes\a\a_orcish_greaves_ul.nif -Decoding meshes\a\a_orcish_cuirass_gnd.nif -Decoding meshes\a\a_orcish_bracer_gnd.nif -Decoding meshes\a\a_orcish_cl_pauldron.nif -Decoding meshes\a\a_orcish_pauldron_gnd.nif -Decoding meshes\f\flora_trama_shrub_05.nif -Decoding meshes\f\flora_trama_shrub_01.nif -Decoding meshes\f\flora_trama_shrub_06.nif -Decoding meshes\f\flora_trama_shrub_02.nif -Decoding meshes\f\flora_trama_shrub_03.nif -Decoding meshes\f\flora_trama_shrub_04.nif -Decoding meshes\f\flora_treestump_wg_01.nif -Decoding meshes\f\flora_treestump_wg_02.nif -Decoding meshes\a\a_ebony_pauldron_gnd.nif -Decoding meshes\a\a_ebony_cuirass_gnd.nif -Decoding meshes\a\a_ebony_pauldron_fa.nif -Decoding meshes\a\a_ebony_pauldron_ua.nif -Decoding meshes\a\a_ebony_pauldron_cl.nif -Decoding meshes\a\a_ebony_cl_pauldron.nif -Decoding meshes\a\a_ebony_greaves_gnd.nif -Decoding meshes\w\w_art_katana_goldbrand.nif -Decoding meshes\m\app_s_calcinator_01.nif -Decoding meshes\m\app_g_calcinator_01.nif -Decoding meshes\m\app_m_calcinator_01.nif -Decoding meshes\m\app_j_calcinator_01.nif -Decoding meshes\b\b_n_dark elf_f_skins.nif -Decoding meshes\b\b_n_dark elf_m_skins.nif -Decoding meshes\b\b_n_dark elf_m_foot.nif -Decoding meshes\b\b_n_dark elf_f_wrist.nif -Decoding meshes\b\b_n_dark elf_m_neck.nif -Decoding meshes\b\b_n_dark elf_f_neck.nif -Decoding meshes\b\b_n_dark elf_m_knee.nif -Decoding meshes\b\b_n_dark elf_f_knee.nif -Decoding meshes\b\b_n_dark elf_f_groin.nif -Decoding meshes\b\b_n_dark elf_m_groin.nif -Decoding meshes\b\b_n_dark elf_m_wrist.nif -Decoding meshes\b\b_n_dark elf_m_ankle.nif -Decoding meshes\b\b_n_dark elf_f_ankle.nif -Decoding meshes\b\b_n_dark elf_f_foot.nif -Decoding meshes\b\b_n_dark elf_m_hair_23.nif -Decoding meshes\b\b_n_dark elf_m_hair_21.nif -Decoding meshes\b\b_n_dark elf_m_hair_25.nif -Decoding meshes\b\b_n_dark elf_f_hair_23.nif -Decoding meshes\b\b_n_dark elf_f_hair_21.nif -Decoding meshes\b\b_n_dark elf_f_head_02.nif -Decoding meshes\b\b_n_dark elf_f_head_06.nif -Decoding meshes\b\b_n_dark elf_f_head_04.nif -Decoding meshes\b\b_n_dark elf_f_head_08.nif -Decoding meshes\b\b_n_dark elf_m_head_02.nif -Decoding meshes\b\b_n_dark elf_m_head_06.nif -Decoding meshes\b\b_n_dark elf_m_head_04.nif -Decoding meshes\b\b_n_dark elf_m_head_08.nif -Decoding meshes\b\b_n_dark elf_m_head_11.nif -Decoding meshes\b\b_n_dark elf_m_head_13.nif -Decoding meshes\b\b_n_dark elf_m_head_15.nif -Decoding meshes\b\b_n_dark elf_m_head_17.nif -Decoding meshes\b\b_n_dark elf_f_hair_09.nif -Decoding meshes\b\b_n_dark elf_f_hair_05.nif -Decoding meshes\b\b_n_dark elf_f_hair_07.nif -Decoding meshes\b\b_n_dark elf_f_hair_01.nif -Decoding meshes\b\b_n_dark elf_f_hair_03.nif -Decoding meshes\b\b_n_dark elf_m_hair_09.nif -Decoding meshes\b\b_n_dark elf_m_hair_05.nif -Decoding meshes\b\b_n_dark elf_m_hair_07.nif -Decoding meshes\b\b_n_dark elf_m_hair_01.nif -Decoding meshes\b\b_n_dark elf_m_hair_03.nif -Decoding meshes\b\b_n_dark elf_f_hair_16.nif -Decoding meshes\b\b_n_dark elf_f_hair_14.nif -Decoding meshes\b\b_n_dark elf_f_hair_12.nif -Decoding meshes\b\b_n_dark elf_f_hair_10.nif -Decoding meshes\b\b_n_dark elf_f_hair_18.nif -Decoding meshes\b\b_n_dark elf_m_hair_16.nif -Decoding meshes\b\b_n_dark elf_m_hair_14.nif -Decoding meshes\b\b_n_dark elf_m_hair_12.nif -Decoding meshes\b\b_n_dark elf_m_hair_10.nif -Decoding meshes\b\b_n_dark elf_m_hair_18.nif -Decoding meshes\b\b_n_dark elf_m_forearm.nif -Decoding meshes\b\b_n_dark elf_m_hair_22.nif -Decoding meshes\b\b_n_dark elf_m_hair_20.nif -Decoding meshes\b\b_n_dark elf_m_hair_26.nif -Decoding meshes\b\b_n_dark elf_m_hair_24.nif -Decoding meshes\b\b_n_dark elf_f_hair_22.nif -Decoding meshes\b\b_n_dark elf_f_hair_20.nif -Decoding meshes\b\b_n_dark elf_f_hair_24.nif -Decoding meshes\b\b_n_dark elf_f_head_03.nif -Decoding meshes\b\b_n_dark elf_f_head_01.nif -Decoding meshes\b\b_n_dark elf_f_head_07.nif -Decoding meshes\b\b_n_dark elf_f_head_05.nif -Decoding meshes\b\b_n_dark elf_f_head_09.nif -Decoding meshes\b\b_n_dark elf_m_head_03.nif -Decoding meshes\b\b_n_dark elf_m_head_01.nif -Decoding meshes\b\b_n_dark elf_m_head_07.nif -Decoding meshes\b\b_n_dark elf_m_head_05.nif -Decoding meshes\b\b_n_dark elf_m_head_09.nif -Decoding meshes\b\b_n_dark elf_f_head_10.nif -Decoding meshes\b\b_n_dark elf_m_head_10.nif -Decoding meshes\b\b_n_dark elf_m_head_12.nif -Decoding meshes\b\b_n_dark elf_m_head_14.nif -Decoding meshes\b\b_n_dark elf_m_head_16.nif -Decoding meshes\b\b_n_dark elf_f_hair_08.nif -Decoding meshes\b\b_n_dark elf_f_hair_04.nif -Decoding meshes\b\b_n_dark elf_f_hair_06.nif -Decoding meshes\b\b_n_dark elf_f_hair_02.nif -Decoding meshes\b\b_n_dark elf_m_hair_08.nif -Decoding meshes\b\b_n_dark elf_m_hair_04.nif -Decoding meshes\b\b_n_dark elf_m_hair_06.nif -Decoding meshes\b\b_n_dark elf_m_hair_02.nif -Decoding meshes\b\b_n_dark elf_f_forearm.nif -Decoding meshes\b\b_n_dark elf_f_hair_17.nif -Decoding meshes\b\b_n_dark elf_f_hair_15.nif -Decoding meshes\b\b_n_dark elf_f_hair_13.nif -Decoding meshes\b\b_n_dark elf_f_hair_11.nif -Decoding meshes\b\b_n_dark elf_f_hair_19.nif -Decoding meshes\b\b_n_dark elf_m_hair_17.nif -Decoding meshes\b\b_n_dark elf_m_hair_15.nif -Decoding meshes\b\b_n_dark elf_m_hair_13.nif -Decoding meshes\b\b_n_dark elf_m_hair_11.nif -Decoding meshes\b\b_n_dark elf_m_hair_19.nif -Decoding meshes\b\b_n_dark elf_m_upper arm.nif -Decoding meshes\b\b_n_dark elf_f_upper leg.nif -Decoding meshes\b\b_n_dark elf_m_upper leg.nif -Decoding meshes\b\b_n_dark elf_f_hands.1st.nif -Decoding meshes\b\b_n_dark elf_m_hands.1st.nif -Decoding meshes\b\b_n_dark elf_f_upper arm.nif -Decoding meshes\a\a_steel_pauldron_gnd.nif -Decoding meshes\a\a_steel_cuirass_gnd.nif -Decoding meshes\a\a_steel_pauldron_fa.nif -Decoding meshes\a\a_steel_pauldron_ua.nif -Decoding meshes\a\a_steel_pauldron_cl.nif -Decoding meshes\a\a_steel_gauntlet_gnd.nif -Decoding meshes\a\a_steel_greaves_gnd.nif -Decoding meshes\a\a_ringmail_cuirass_gnd.nif -Decoding meshes\b\b_n_nord_m_hands.1st.nif -Decoding meshes\b\b_n_nord_f_upper leg.nif -Decoding meshes\b\b_n_nord_f_hands.1st.nif -Decoding meshes\b\b_n_nord_m_upper leg.nif -Decoding meshes\b\b_n_nord_m_upper arm.nif -Decoding meshes\b\b_n_nord_f_upper arm.nif -Decoding meshes\a\towershield_telvanni.nif -Decoding meshes\a\towershield_bonemold.nif -Decoding meshes\a\towershield_daedric.nif -Decoding meshes\a\towershield_redoranm.nif -Decoding meshes\a\towershield_trollbone.nif -Decoding meshes\a\towershield_dragonscale.nif -Decoding meshes\a\towershield_netch_leather.nif -Decoding meshes\l\light_torch_small_01.nif -Decoding meshes\m\misc_candle_blue_01.nif -Decoding meshes\m\misc_candle_ivory_01.nif -Decoding meshes\m\misc_candle_green_01.nif -Decoding meshes\m\misc_paper_plain_01.nif -Decoding meshes\w\w_shortsword_chitin.nif -Decoding meshes\w\w_shortsword_daedric.nif -Decoding meshes\w\w_shortsword_imperial.nif -Decoding meshes\a\a_boots_heavy_leather.nif -Decoding meshes\w\w_nordic_broadsword.nif -Decoding meshes\b\b_v_redguard_m_head_01.nif -Decoding meshes\b\b_v_redguard_f_head_01.nif -Decoding meshes\c\c_art_ring_vampiric.nif -Decoding meshes\c\c_art_ring_phynaster.nif -Decoding meshes\c\c_art_ring_denstagmer.nif -Decoding meshes\c\c_art_ring_surrounding.nif -Decoding meshes\m\furn_com_coatofarms_01.nif -Decoding meshes\m\probe_secretmaster_01.nif -Decoding meshes\b\b_v_breton_m_head_01.nif -Decoding meshes\b\b_v_breton_f_head_01.nif -Decoding meshes\w\w_broadsword_leafblade.nif -Decoding meshes\w\w_broadsword_imperial.nif -Decoding meshes\f\xfurn_banner_tavern_01.nif -Decoding meshes\f\xfurn_banner_temple_03.nif -Decoding meshes\f\xfurn_banner_temple_01.nif -Decoding meshes\f\xfurn_bannerd_goods_01.nif -Decoding meshes\f\xfurn_banner_hlaalu_01.nif -Decoding meshes\f\xfurn_banner_dagoth_01.nif -Decoding meshes\f\xfurn_banner_temple_02.nif -Decoding meshes\f\xfurn_bannerd_welcome_01.nif -Decoding meshes\f\xfurn_bannerd_wa_shop_01.nif -Decoding meshes\f\xfurn_bannerd_alchemy_01.nif -Decoding meshes\f\xfurn_bannerd_danger_01.nif -Decoding meshes\m\misc_bowl_redware_01.nif -Decoding meshes\m\misc_bowl_redware_03.nif -Decoding meshes\m\misc_bowl_redware_02.nif -Decoding meshes\m\misc_bowl_bugdesign_01.nif -Decoding meshes\m\misc_bowl_glass_peach_01.nif -Decoding meshes\m\misc_com_tankard_01.nif -Decoding meshes\m\misc_com_wood_cup_02.nif -Decoding meshes\m\misc_com_wood_cup_03.nif -Decoding meshes\m\misc_com_wood_knife.nif -Decoding meshes\m\misc_com_wood_cup_01.nif -Decoding meshes\m\misc_com_iron_ladle.nif -Decoding meshes\m\misc_com_wood_cup_04.nif -Decoding meshes\m\misc_com_wood_spoon_01.nif -Decoding meshes\m\misc_com_wood_bowl_03.nif -Decoding meshes\m\misc_com_wood_bowl_01.nif -Decoding meshes\m\misc_com_bucket_metal.nif -Decoding meshes\m\misc_com_wood_spoon_02.nif -Decoding meshes\m\misc_com_wood_bowl_02.nif -Decoding meshes\m\misc_com_wood_bowl_05.nif -Decoding meshes\m\misc_com_wood_bowl_04.nif -Decoding meshes\m\misc_com_metal_plate_04.nif -Decoding meshes\m\misc_com_silverware_fork.nif -Decoding meshes\m\misc_com_metal_goblet_02.nif -Decoding meshes\m\misc_com_metal_goblet_01.nif -Decoding meshes\m\misc_com_metal_plate_03.nif -Decoding meshes\m\misc_com_metal_plate_07.nif -Decoding meshes\m\misc_com_metal_plate_05.nif -Decoding meshes\f\furn_redoran_flag_in.nif -Decoding meshes\f\furn_redoran_flag_01.nif -Decoding meshes\f\furn_redoran_hearth_02.nif -Decoding meshes\f\furn_redoran_shelf_03.nif -Decoding meshes\f\furn_redoran_hearth_01.nif -Decoding meshes\f\furn_redoran_shelf2_01.nif -Decoding meshes\f\furn_redoran_shelf1_01.nif -Decoding meshes\m\misc_mortarpestle_01.nif -Decoding meshes\m\misc_mortarpestle_s_01.nif -Decoding meshes\m\misc_mortarpestle_g_01.nif -Decoding meshes\m\misc_mortarpestle_a_01.nif -Decoding meshes\m\misc_mortarpestle_m_01.nif -Decoding meshes\f\furn_velothi_altar_01.nif -Decoding meshes\m\misc_soulgem_lesser.nif -Decoding meshes\m\misc_soulgem_common.nif -Decoding meshes\m\misc_soulgem_greater.nif -Decoding meshes\m\misc_potion_fresh_01.nif -Decoding meshes\m\misc_potion_cheap_01.nif -Decoding meshes\m\misc_pot_mottled_01.nif -Decoding meshes\m\misc_pot_redware_02.nif -Decoding meshes\m\misc_pot_redware_03.nif -Decoding meshes\m\misc_pot_redware_01.nif -Decoding meshes\m\misc_pot_redware_04.nif -Decoding meshes\m\misc_potion_bargain_01.nif -Decoding meshes\m\misc_potion_quality_01.nif -Decoding meshes\m\misc_potion_exclusive_01.nif -Decoding meshes\m\misc_potion_standard_01.nif -Decoding meshes\m\misc_pot_glass_peach_01.nif -Decoding meshes\m\misc_pot_glass_peach_02.nif -Decoding meshes\f\furn_de_winerack_01.nif -Decoding meshes\f\furn_de_bookshelf_01.nif -Decoding meshes\f\furn_de_practice_mat.nif -Decoding meshes\f\furn_de_signpost_04.nif -Decoding meshes\f\furn_de_signpost_01.nif -Decoding meshes\f\furn_de_signpost_03.nif -Decoding meshes\f\furn_de_signpost_02.nif -Decoding meshes\f\furn_de_tapestry_10.nif -Decoding meshes\f\furn_de_tapestry_01.nif -Decoding meshes\f\furn_de_tapestry_11.nif -Decoding meshes\f\furn_de_tapestry_02.nif -Decoding meshes\f\furn_de_tapestry_12.nif -Decoding meshes\f\furn_de_tapestry_03.nif -Decoding meshes\f\furn_de_tapestry_13.nif -Decoding meshes\f\furn_de_tapestry_04.nif -Decoding meshes\f\furn_de_tapestry_05.nif -Decoding meshes\f\furn_de_tapestry_06.nif -Decoding meshes\f\furn_de_tapestry_07.nif -Decoding meshes\f\furn_de_tapestry_08.nif -Decoding meshes\f\furn_de_tapestry_09.nif -Decoding meshes\f\furn_de_ex_table_02.nif -Decoding meshes\f\furn_de_ex_table_03.nif -Decoding meshes\f\furn_de_bookshelf_02.nif -Decoding meshes\f\furn_de_firepit_f_01.nif -Decoding meshes\f\furn_de_ex_bench_01.nif -Decoding meshes\f\furn_de_ex_stool_02.nif -Decoding meshes\f\furn_de_tapestry_m_01.nif -Decoding meshes\f\furn_de_banner_book_01.nif -Decoding meshes\f\furn_de_banner_book_in.nif -Decoding meshes\f\furn_de_banner_pawn_01.nif -Decoding meshes\f\furn_de_shack_basket_01.nif -Decoding meshes\f\furn_de_shack_basket_02.nif -Decoding meshes\m\misc_com_pitcher_metal_01.nif -Decoding meshes\m\misc_com_silverware_spoon.nif -Decoding meshes\m\misc_com_silverware_knife.nif -Decoding meshes\f\furn_de_banner_telvani_in.nif -Decoding meshes\f\furn_de_banner_telvani_01.nif -Decoding meshes\f\xfurn_bannerd_clothing_01.nif -Decoding meshes\m\misc_bowl_orange_green_01.nif -Decoding meshes\m\misc_bowl_glass_yellow_01.nif -Decoding meshes\o\flora_red_lichen_01.nif -Decoding meshes\o\flora_red_lichen_02.nif -Decoding meshes\o\flora_red_lichen_03.nif -Decoding meshes\m\misc_glass_yellow_01.nif -Decoding meshes\m\misc_glass_green_01.nif -Decoding meshes\r\guar.nif -Decoding meshes\r\xazura.nif -Decoding meshes\r\shalk.nif -Decoding meshes\r\azura.nif -Decoding meshes\r\dreugh.nif -Decoding meshes\r\hunger.nif -Decoding meshes\r\xguar.nif -Decoding meshes\r\xshalk.nif -Decoding meshes\raindrop.nif -Decoding meshes\r\rust rat.nif -Decoding meshes\r\skeleton.nif -Decoding meshes\r\xdagothr.nif -Decoding meshes\r\xdreugh.nif -Decoding meshes\r\xbyagram.nif -Decoding meshes\r\xdremora.nif -Decoding meshes\r\xhunger.nif -Decoding meshes\r\daedroth.nif -Decoding meshes\r\dremora.nif -Decoding meshes\r\dagothr.nif -Decoding meshes\r\bonelord.nif -Decoding meshes\r\byagram.nif -Decoding meshes\r\ashghoul.nif -Decoding meshes\r\ashslave.nif -Decoding meshes\r\nixhound.nif -Decoding meshes\rainsplash.nif -Decoding meshes\r\bonewalker.nif -Decoding meshes\r\lordvivec.nif -Decoding meshes\r\xnixhound.nif -Decoding meshes\r\xminescrib.nif -Decoding meshes\r\xlordvivec.nif -Decoding meshes\r\cliffracer.nif -Decoding meshes\r\clannfear.nif -Decoding meshes\r\minescrib.nif -Decoding meshes\right_arrow.nif -Decoding meshes\r\netch_bull.nif -Decoding meshes\r\xdaedroth.nif -Decoding meshes\r\xduskyalit.nif -Decoding meshes\r\xclannfear.nif -Decoding meshes\r\xbabelfish.nif -Decoding meshes\r\xbonelord.nif -Decoding meshes\r\xashzombie.nif -Decoding meshes\r\xashghoul.nif -Decoding meshes\r\xashslave.nif -Decoding meshes\r\babelfish.nif -Decoding meshes\r\guar_white.nif -Decoding meshes\r\duskyalit.nif -Decoding meshes\r\xskeleton.nif -Decoding meshes\r\ashvampire.nif -Decoding meshes\r\ashzombie.nif -Decoding meshes\r\xrust rat.nif -Decoding meshes\b\b_v_argonian_m_head_01.nif -Decoding meshes\b\b_v_argonian_f_head_01.nif -Decoding meshes\b\b_v_high elf_f_head_01.nif -Decoding meshes\b\b_v_high elf_m_head_01.nif -Decoding meshes\o\flora_stoneflower_02.nif -Decoding meshes\o\flora_stoneflower_01.nif -Decoding meshes\a\a_art_shield_breaker.nif -Decoding meshes\slider_bar.nif -Decoding meshes\smoke_green.nif -Decoding meshes\sky_night_01.nif -Decoding meshes\w\w_art_blade_crescent.nif -Decoding meshes\torchfire.nif -Decoding meshes\w\shadowblunttwoclose.nif -Decoding meshes\c\artifact_bloodring_01.nif -Decoding meshes\c\artifact_belt_hfire_01.nif -Decoding meshes\c\artifact_ring_soul_01.nif -Decoding meshes\c\artifact_amulet_hring_01.nif -Decoding meshes\c\artifact_amulet_hheal_01.nif -Decoding meshes\c\artifact_amulet_hfire_01.nif -Decoding meshes\a\a_art_towershield_eleidon.nif -Decoding meshes\c\artifact_amulet_hthrum_01.nif -Decoding meshes\c\artifact_amulet_htrime_01.nif -Decoding meshes\a\a_glass_pauldron_gnd.nif -Decoding meshes\a\a_glass_cuirass_gnd.nif -Decoding meshes\a\a_glass_pauldron_fa.nif -Decoding meshes\a\a_glass_pauldron_ua.nif -Decoding meshes\a\a_glass_cl_pauldron.nif -Decoding meshes\a\a_glass_greaves_gnd.nif -Decoding meshes\a\a_glass_cl_pauldron_gnd.nif -Decoding meshes\b\b_v_khajiit_m_head_01.nif -Decoding meshes\b\b_v_khajiit_f_head_01.nif -Decoding meshes\f\furn_rail_straight_00.nif -Decoding meshes\w\w_art_claymore_umbra.nif -Decoding meshes\w\w_art_cleaverstfelms.nif -Decoding meshes\w\w_art_crosierstlloth.nif -Decoding meshes\w\w_art_claymore_chrys.nif -Decoding meshes\w\w_art_claymore_iceblade.nif -Decoding meshes\f\furn_lavacave_pool00.nif -Decoding meshes\f\furn_lavacave_spout00.nif -Decoding meshes\f\furn_banner_hlaalu_01.nif -Decoding meshes\f\furn_banner_tavern_in.nif -Decoding meshes\f\furn_banner_temple_01.nif -Decoding meshes\f\furn_bannerd_danger_in.nif -Decoding meshes\f\furn_banner_tavern_01.nif -Decoding meshes\f\furn_banner_temple_04.nif -Decoding meshes\f\furn_bannerd_goods_in.nif -Decoding meshes\f\furn_bannerd_danger_01.nif -Decoding meshes\f\furn_banner_temple_03.nif -Decoding meshes\f\furn_bannerd_goods_01.nif -Decoding meshes\f\furn_banner_dagoth_01.nif -Decoding meshes\f\furn_banner_temple_02.nif -Decoding meshes\f\furn_bannerd_alchemy_01.nif -Decoding meshes\f\furn_bannerd_wa_shop_01.nif -Decoding meshes\f\furn_bannerd_welcome_01.nif -Decoding meshes\f\furn_bannerd_clothing_01.nif -Decoding meshes\f\furn_bannerd_wa_shop_in.nif -Decoding meshes\f\furn_bannerd_alchemy_in.nif -Decoding meshes\f\furn_bannerd_clothing_in.nif -Decoding meshes\f\furn_banner_temple_01_in.nif -Decoding meshes\f\furn_banner_temple_02_in.nif -Decoding meshes\f\furn_banner_temple_03_in.nif -Decoding meshes\f\furn_dae_rubble_01c.nif -Decoding meshes\f\furn_dae_rubble_01b.nif -Decoding meshes\f\furn_dae_rubble_03b.nif -Decoding meshes\f\furn_dae_rubble_01a.nif -Decoding meshes\f\furn_dae_rubble_03a.nif -Decoding meshes\f\furn_dae_rubble_04a.nif -Decoding meshes\f\furn_dae_rubble_pointy.nif -Decoding meshes\upper_arrow.nif -Decoding meshes\a\a_studdedleather_c_gnd.nif -Decoding meshes\a\a_studdedleather_cuirass.nif -Decoding meshes\w\w_dwemer_shortsword.nif -Decoding meshes\m\misc_chest_small_02.nif -Decoding meshes\m\misc_chest_small_01.nif -Decoding meshes\m\text_octavo_open_08.nif -Decoding meshes\m\text_octavo_open_04.nif -Decoding meshes\m\text_octavo_open_06.nif -Decoding meshes\m\text_octavo_open_07.nif -Decoding meshes\m\text_octavo_open_01.nif -Decoding meshes\m\text_octavo_open_02.nif -Decoding meshes\m\text_octavo_open_03.nif -Decoding meshes\f\xex_ashl_e_banner_r.nif -Decoding meshes\f\xex_ashl_a_banner_r.nif -Decoding meshes\f\xex_ashl_z_banner_r.nif -Decoding meshes\f\xex_ashl_u_banner_r.nif -Decoding meshes\m\text_scroll_open_01.nif -Decoding meshes\m\text_scroll_open_02.nif -Decoding meshes\m\text_scroll_open_03.nif -Decoding meshes\a\a_silver_cuirass_duke.nif -Decoding meshes\a\a_daedric_greaves_g.nif -Decoding meshes\a\a_daedric_greaves_ul.nif -Decoding meshes\a\a_daedric_hands.1st.nif -Decoding meshes\a\a_daedric_fountain_h.nif -Decoding meshes\a\a_daedric_boots_gnd.nif -Decoding meshes\a\a_daedric_pauldron_gnd.nif -Decoding meshes\a\a_daedric_pauldron_ua.nif -Decoding meshes\a\a_daedric_greaves_gnd.nif -Decoding meshes\a\a_daedric_pauldron_cl.nif -Decoding meshes\a\a_daedric_cuirass_gnd.nif -Decoding meshes\a\a_daedric_terrifying_h.nif -Decoding meshes\a\a_daedric_gauntlet_gnd.nif -Decoding meshes\f\xfurn_de_banner_book_01.nif -Decoding meshes\f\xfurn_de_banner_pawn_01.nif -Decoding meshes\o\flora_willow_flower_02.nif -Decoding meshes\o\flora_willow_flower_01.nif -Decoding meshes\f\furn_screen_guar_01.nif -Decoding meshes\a\a_art_wraithguard.1st.nif -Decoding meshes\a\a_art_wraithguard_gnd.nif -Decoding meshes\w\w_mace.nif -Decoding meshes\w\w_bolt01.nif -Decoding meshes\w\w_club00.nif -Decoding meshes\w\w_spear.nif -Decoding meshes\w\w_tanto.nif -Decoding meshes\w\w_saber.nif -Decoding meshes\m\misc_vivec_ashmask_01.nif -Decoding meshes\m\misc_silverware_bowl.nif -Decoding meshes\m\misc_silverware_cup.nif -Decoding meshes\m\misc_silverware_cup_01.nif -Decoding meshes\m\misc_silverware_pitcher.nif -Decoding meshes\m\misc_silverware_plate_01.nif -Decoding meshes\m\misc_silverware_plate_03.nif -Decoding meshes\m\misc_silverware_plate_02.nif -Decoding meshes\w\w_staff00.nif -Decoding meshes\w\w_crossbow.nif -Decoding meshes\w\w_longbow.nif -Decoding meshes\w\w_claymore.nif -Decoding meshes\w\w_n_katana.nif -Decoding meshes\w\w_de_fork.nif -Decoding meshes\w\w_arrow01.nif -Decoding meshes\f\xfurn_de_banner_telvani_01.nif -Decoding meshes\d\ex_de_ship_trapdoor.nif -Decoding meshes\d\in_de_shipdoor_toplevel.nif -Decoding meshes\x\ex_t_tower_seedling.nif -Decoding meshes\x\ex_t_tower_strght_lrg.nif -Decoding meshes\x\ex_stronghold_fort00.nif -Decoding meshes\x\ex_strongruin_fort01.nif -Decoding meshes\x\ex_stronghold_door10.nif -Decoding meshes\x\ex_stronghold_wall00.nif -Decoding meshes\x\ex_stronghold_wall02.nif -Decoding meshes\x\ex_stronghold_fort02.nif -Decoding meshes\x\ex_strongruin_fort05.nif -Decoding meshes\x\ex_stronghold_dome00.nif -Decoding meshes\x\ex_strongruin_fort02.nif -Decoding meshes\x\ex_strongruin_fort00.nif -Decoding meshes\x\ex_stronghold_wall03.nif -Decoding meshes\x\ex_stronghold_fort03.nif -Decoding meshes\x\ex_stronghold_fort05.nif -Decoding meshes\x\ex_stronghold_fort01.nif -Decoding meshes\x\ex_stronghold_wall01.nif -Decoding meshes\x\ex_strongruin_dome00.nif -Decoding meshes\x\ex_strongruin_fort03.nif -Decoding meshes\x\ex_stronghold_pylon01.nif -Decoding meshes\x\ex_stronghold_enter00.nif -Decoding meshes\x\ex_stronghold_window00.nif -Decoding meshes\x\ex_stronghold_pylon02.nif -Decoding meshes\x\ex_strong_roofstack00.nif -Decoding meshes\x\ex_stronghold_pylon00.nif -Decoding meshes\x\ex_strongruin_enter00.nif -Decoding meshes\x\ex_t_root_bridge_01.nif -Decoding meshes\x\ex_t_root_spikes_01.nif -Decoding meshes\x\ex_t_root_spikes_02.nif -Decoding meshes\x\ex_t_rock_coastal_03.nif -Decoding meshes\x\ex_t_rock_coastal_01.nif -Decoding meshes\x\ex_t_rock_coastal_02.nif -Decoding meshes\x\ex_t_root_lendsplit.nif -Decoding meshes\x\ex_strongholdruin_wall00.nif -Decoding meshes\x\ex_strongholdruin_wall01.nif -Decoding meshes\x\ex_strongholdruin_wall02.nif -Decoding meshes\x\ex_strongholdruin_wall03.nif -Decoding meshes\x\ex_strongruin_smdwell00.nif -Decoding meshes\x\ex_stronghold_smdwell00.nif -Decoding meshes\x\ex_stronghold_sandpit00.nif -Decoding meshes\x\in_t_housepod_2flr_stair.nif -Decoding meshes\d\in_impsmall_door_01.nif -Decoding meshes\d\in_impsmall_d_cave_01.nif -Decoding meshes\d\in_impsmall_d_hidden_01.nif -Decoding meshes\d\in_impsmall_loaddoor_01.nif -Decoding meshes\d\in_impsmall_door_jail_01.nif -Decoding meshes\d\in_impsmall_door_jail_02.nif -Decoding meshes\x\ex_redoran_tower_01.nif -Decoding meshes\x\ex_redoran_window_01.nif -Decoding meshes\x\ex_redoran_window_02.nif -Decoding meshes\x\ex_redoran_tavern_01.nif -Decoding meshes\x\ex_redoran_steps_01.nif -Decoding meshes\x\ex_redoran_steps_02.nif -Decoding meshes\x\ex_redoran_awning_01.nif -Decoding meshes\x\ex_redoran_barracks_01.nif -Decoding meshes\x\ex_redoran_building_02.nif -Decoding meshes\x\ex_redoran_building_03.nif -Decoding meshes\x\ex_redoran_building_01.nif -Decoding meshes\x\ex_t_doorway_sphere_01.nif -Decoding meshes\x\ex_redoran_building_01a.nif -Decoding meshes\x\ex_velothi_window_01.nif -Decoding meshes\x\ex_velothi_temple_02.nif -Decoding meshes\x\ex_velothi_temple_01.nif -Decoding meshes\x\ex_velothi_tower_01.nif -Decoding meshes\x\ex_velothi_triwin_01.nif -Decoding meshes\x\ex_velothi_hilltent_01.nif -Decoding meshes\x\ex_velothi_entrance_03.nif -Decoding meshes\x\ex_velothi_entrance_01.nif -Decoding meshes\x\ex_velothi_entrance_02.nif -Decoding meshes\x\ex_velothi_tower_01_a.nif -Decoding meshes\x\ex_velothi_striderport_01.nif -Decoding meshes\x\ex_strongruin_fort05_half.nif -Decoding meshes\x\ex_redoran_striderport_01.nif -Decoding meshes\c\amulet_extravagant_1.nif -Decoding meshes\c\amulet_thongofzainab.nif -Decoding meshes\c\amulet_extravagant_2.nif -Decoding meshes\c\amulet_teeth_urshilaku.nif -Decoding meshes\x\ex_common_skywalk_01.nif -Decoding meshes\x\ex_common_window_01.nif -Decoding meshes\x\ex_common_window_02.nif -Decoding meshes\x\ex_common_window_03.nif -Decoding meshes\x\ex_common_plat_cent.nif -Decoding meshes\x\ex_common_plat_rail.nif -Decoding meshes\x\ex_common_plat_corn.nif -Decoding meshes\x\ex_common_tavern_01.nif -Decoding meshes\x\ex_common_lighthouse.nif -Decoding meshes\x\ex_common_balcony_01.nif -Decoding meshes\x\ex_common_dormer_round.nif -Decoding meshes\x\ex_common_house_addon.nif -Decoding meshes\x\ex_common_building_01.nif -Decoding meshes\x\ex_common_building_03.nif -Decoding meshes\x\ex_common_tower_thatch.nif -Decoding meshes\x\ex_common_chimney_tall.nif -Decoding meshes\x\ex_common_entrance_02.nif -Decoding meshes\x\ex_common_building_02.nif -Decoding meshes\x\ex_common_entrance_01.nif -Decoding meshes\x\ex_common_dormer_square.nif -Decoding meshes\x\ex_common_house_tall_01.nif -Decoding meshes\x\ex_common_awning_wood_01.nif -Decoding meshes\x\ex_common_house_tall_02.nif -Decoding meshes\x\ex_common_trellis_withvine.nif -Decoding meshes\x\ex_common_house_mixedroofs.nif -Decoding meshes\x\ex_t_playertower_sprout.nif -Decoding meshes\f\furn_t_fireplace_01.nif -Decoding meshes\f\furn_c_t_dibella_01.nif -Decoding meshes\f\furn_c_t_stendarr_01.nif -Decoding meshes\f\furn_c_t_akatosh_01.nif -Decoding meshes\f\furn_c_t_warrior_01.nif -Decoding meshes\f\furn_c_t_kynareth_01.nif -Decoding meshes\f\furn_c_t_julianos_01.nif -Decoding meshes\f\furn_c_t_zenithar_01.nif -Decoding meshes\f\furn_c_t_apprentice_01.nif -Decoding meshes\f\active_blight_medium.nif -Decoding meshes\f\active_triolith_01a.nif -Decoding meshes\f\active_sign_c_inn_02.nif -Decoding meshes\f\active_com_bar_door.nif -Decoding meshes\f\active_blight_large.nif -Decoding meshes\f\active_blight_small.nif -Decoding meshes\f\active_sign_c_goods_02.nif -Decoding meshes\f\active_sign_c_pwan_01.nif -Decoding meshes\f\active_sign_c_arms_01.nif -Decoding meshes\f\active_sign_c_goods_01.nif -Decoding meshes\f\active_sign_c_arms_02.nif -Decoding meshes\f\active_sign_c_alchemy_01.nif -Decoding meshes\f\active_sign_c_guildm_01.nif -Decoding meshes\f\active_sign_c_guildf_01.nif -Decoding meshes\f\active_sign_c_clothing_01.nif -Decoding meshes\c\c_ring_extravagant_1.nif -Decoding meshes\c\c_ring_extravagant_2.nif -Decoding meshes\x\ex_v_vivecstatue_01.nif -Decoding meshes\x\ex_v_vivecstatue_02.nif -Decoding meshes\x\ex_v_sign_stdeyln_01.nif -Decoding meshes\x\ex_v_sign_stolms_01.nif -Decoding meshes\x\ex_v_sign_hlaalu_01.nif -Decoding meshes\x\ex_v_sign_redoran_01.nif -Decoding meshes\x\ex_v_sign_telvanni_01.nif -Decoding meshes\x\ex_lighthouse_stone.nif -Decoding meshes\x\ex_c_chimney_tall_02.nif -Decoding meshes\x\ex_gg_gateswitch_01.nif -Decoding meshes\x\ex_gg_gatetriolith_01.nif -Decoding meshes\i\in_dwrv_tower_int00.nif -Decoding meshes\w\w_6th_hammer.nif -Decoding meshes\x\ex_cavern_padlock00.nif -Decoding meshes\x\ex_cave_coastrock00.nif -Decoding meshes\x\ex_cave_entrance_10.nif -Decoding meshes\x\ex_vivec_w_slope_01.nif -Decoding meshes\x\ex_vivec_ent_telt_01.nif -Decoding meshes\x\ex_vivec_buttress_01.nif -Decoding meshes\x\ex_vivec_p_water_01.nif -Decoding meshes\x\ex_vivec_b_gap_t_01.nif -Decoding meshes\x\ex_vivec_b_gap_b_01.nif -Decoding meshes\x\ex_vivec_b_gap_b_02.nif -Decoding meshes\x\ex_vivec_b_wb_gap_01.nif -Decoding meshes\x\ex_vivec_wspout_d_02.nif -Decoding meshes\x\ex_vivec_bridgew_01.nif -Decoding meshes\x\ex_vivec_waterfall_03.nif -Decoding meshes\x\ex_vivec_waterfall_05.nif -Decoding meshes\x\ex_vivec_prisonmoon_01.nif -Decoding meshes\x\ex_vivec_waterspout_02.nif -Decoding meshes\x\ex_vivec_waterfall_01.nif -Decoding meshes\x\ex_vivec_bridgewgap_01.nif -Decoding meshes\x\ex_vivec_waterspout_05.nif -Decoding meshes\x\ex_vivec_waterspout_01.nif -Decoding meshes\x\ex_vivec_waterspout_03.nif -Decoding meshes\x\ex_waterfall_mist_01.nif -Decoding meshes\x\ex_waterfall_mist_s_01.nif -Decoding meshes\x\ex_ropebridge_512_01.nif -Decoding meshes\x\ex_ropebridge_1024_01.nif -Decoding meshes\x\ex_ropebridge_2048_01.nif -Decoding meshes\x\ex_ropebridge_stake_01.nif -Decoding meshes\i\in_t_stairs_strt_256.nif -Decoding meshes\i\in_c_stone_room_side.nif -Decoding meshes\i\in_c_stone_room_corner.nif -Decoding meshes\i\in_c_stone_room_center.nif -Decoding meshes\i\in_c_stone_room_entry.nif -Decoding meshes\i\in_c_stone_stair_short.nif -Decoding meshes\i\in_c_stone_hall_small.nif -Decoding meshes\i\in_t_stairs_strt_wiz_256.nif -Decoding meshes\i\in_c_stair_rich_tall_01.nif -Decoding meshes\i\in_c_stair_rich_pend_01.nif -Decoding meshes\i\in_c_stair_plain_tall_01.nif -Decoding meshes\i\in_c_stair_plain_tall_02.nif -Decoding meshes\i\in_c_stair_rich_tall_02.nif -Decoding meshes\i\in_c_stair_rich_pend_02.nif -Decoding meshes\i\in_c_stone_room_c_con_01.nif -Decoding meshes\m\repair_journeyman_01.nif -Decoding meshes\m\repair_secretmaster_01.nif -Decoding meshes\m\repair_grandmaster_01.nif -Decoding meshes\x\ex_t_menhir_crystal.nif -Decoding meshes\i\in_c_stair_thatch_pend_01.nif -Decoding meshes\i\in_c_stair_thatch_pend_02.nif -Decoding meshes\i\in_c_stair_thatch_tall_01.nif -Decoding meshes\i\in_c_stair_thatch_tall_02.nif -Decoding meshes\i\in_c_stairs_rich_ptall_02.nif -Decoding meshes\i\in_c_stairs_rich_ptall_01.nif -Decoding meshes\x\ex_de_docks_centerb.nif -Decoding meshes\x\ex_de_docks_centers.nif -Decoding meshes\x\ex_de_docks_centersb.nif -Decoding meshes\x\ex_de_docks_steps_01.nif -Decoding meshes\x\ex_de_docks_pilings.nif -Decoding meshes\x\ex_de_docks_pilingb.nif -Decoding meshes\x\ex_de_docks_cornerb_01.nif -Decoding meshes\x\ex_de_docks_corners_03.nif -Decoding meshes\x\ex_de_docks_corners_01.nif -Decoding meshes\x\ex_de_docks_corner_02.nif -Decoding meshes\x\ex_de_docks_cornerb_02.nif -Decoding meshes\x\ex_de_docks_corners_02.nif -Decoding meshes\x\ex_de_docks_corner_01.nif -Decoding meshes\x\ex_de_docks_piling_01.nif -Decoding meshes\x\ex_de_docks_cornersb_02.nif -Decoding meshes\x\ex_de_docks_cornersb_01.nif -Decoding meshes\x\ex_de_docks_cornersb_03.nif -Decoding meshes\x\ex_nord_doorrocks_01.nif -Decoding meshes\x\ex_nord_houseshed_01.nif -Decoding meshes\x\ex_daed_wall_512_01.nif -Decoding meshes\x\ex_daed_pillar_claw_01.nif -Decoding meshes\x\ex_bc_cave_entrance.nif -Decoding meshes\x\ex_ac_cave_entrance_01.nif -Decoding meshes\x\ex_bc_cave_entrance_01.nif -Decoding meshes\x\ex_ma_cave_entrance.nif -Decoding meshes\x\ex_ma_cave_entrance_01.nif -Decoding meshes\x\ex_wg_cave_entrance_01.nif -Decoding meshes\x\ex_de_cave_entrance_01.nif -Decoding meshes\x\ex_ai_cave_entrance_01.nif -Decoding meshes\x\ex_rm_cave_entrance_01.nif -Decoding meshes\x\ex_gl_cave_entrance_01.nif -Decoding meshes\x\ex_al_cave_entrance_01.nif -Decoding meshes\x\ex_ma_cave_entrance_lava.nif -Decoding meshes\i\in_ar_shellbottom_01.nif -Decoding meshes\i\in_de_shack_trapdoor.nif -Decoding meshes\i\in_de_shipwreckll_lg.nif -Decoding meshes\i\in_de_shipwreck_top.nif -Decoding meshes\i\in_de_shipwreckul_lg.nif -Decoding meshes\i\in_de_ship_upperlevel.nif -Decoding meshes\i\in_de_ship_lowerlevel.nif -Decoding meshes\i\in_de_shack_trapdoor_01.nif -Decoding meshes\i\in_t_ls_hall_connect.nif -Decoding meshes\i\in_t_ls_hall_connect_01.nif -Decoding meshes\n\potion_local_brew_01.nif -Decoding meshes\n\potion_t_bug_musk_01.nif -Decoding meshes\n\potion_local_liquor_01.nif -Decoding meshes\n\potion_cyro_brandy_01.nif -Decoding meshes\n\potion_cyro_whiskey_01.nif -Decoding meshes\n\potion_comberry_wine_01.nif -Decoding meshes\i\in_impsmall_hall_02.nif -Decoding meshes\i\in_impsmall_hall_03.nif -Decoding meshes\i\in_impsmall_hall_01.nif -Decoding meshes\i\in_impsmall_wall_01.nif -Decoding meshes\i\in_impsmall_3way_01.nif -Decoding meshes\i\in_impsmall_4way_01.nif -Decoding meshes\i\in_impsmall_door_01.nif -Decoding meshes\i\in_impsmall_r_3way_01.nif -Decoding meshes\i\in_impsmall_r_entr_04.nif -Decoding meshes\i\in_impsmall_r_entr_01.nif -Decoding meshes\i\in_impsmall_r_entr_02.nif -Decoding meshes\i\in_impsmall_corner_01.nif -Decoding meshes\i\in_impsmall_shutter_01.nif -Decoding meshes\i\in_impsmall_dj_cave_01.nif -Decoding meshes\i\in_impsmall_endcap_01.nif -Decoding meshes\i\in_impsmall_spiral_01.nif -Decoding meshes\i\in_impsmall_doorjam_01.nif -Decoding meshes\i\in_impsmall_stairs_01.nif -Decoding meshes\i\in_impsmall_r_entr_03.nif -Decoding meshes\i\in_impsmall_r_corner_02.nif -Decoding meshes\i\in_impsmall_trapdoor_01a.nif -Decoding meshes\i\in_impsmall_r_pillar_01.nif -Decoding meshes\i\in_impsmall_dj_hidden_01.nif -Decoding meshes\i\in_impsmall_r_corner_01.nif -Decoding meshes\i\in_impsmall_r_corner_03.nif -Decoding meshes\i\in_impsmall_trapdoor_01.nif -Decoding meshes\i\in_impsmall_r_center_01.nif -Decoding meshes\n\potion_comberry_brandy_01.nif -Decoding meshes\i\in_impsmall_spiral_end_01.nif -Decoding meshes\i\in_impsmall_spiral_bot_01.nif -Decoding meshes\x\ex_lavacave_spout10.nif -Decoding meshes\x\ex_v_palace_steps_01.nif -Decoding meshes\x\ex_v_ban_redoran_01.nif -Decoding meshes\x\ex_v_ban_telvanni_01.nif -Decoding meshes\x\ex_v_ban_stdeyln_01.nif -Decoding meshes\x\ex_v_ban_serving_01.nif -Decoding meshes\x\ex_v_ban_comfort_01.nif -Decoding meshes\x\ex_v_ban_tribunal_01.nif -Decoding meshes\x\ex_hlaalu_bridge_06.nif -Decoding meshes\x\ex_hlaalu_bridge_07.nif -Decoding meshes\x\ex_hlaalu_bridge_04.nif -Decoding meshes\x\ex_hlaalu_bridge_05.nif -Decoding meshes\x\ex_hlaalu_bridge_02.nif -Decoding meshes\x\ex_hlaalu_bridge_03.nif -Decoding meshes\x\ex_hlaalu_bridge_10.nif -Decoding meshes\x\ex_hlaalu_bridge_01.nif -Decoding meshes\x\ex_hlaalu_wall_up_01.nif -Decoding meshes\x\ex_hlaalu_dsteps_01.nif -Decoding meshes\x\ex_hlaalu_dsteps_02.nif -Decoding meshes\x\ex_hlaalu_dsteps_03.nif -Decoding meshes\x\ex_hlaalu_wall_up_02.nif -Decoding meshes\x\ex_hlaalu_balcony_02.nif -Decoding meshes\x\ex_hlaalu_balcony_01.nif -Decoding meshes\x\ex_hlaalu_buttress_04.nif -Decoding meshes\x\ex_hlaalu_wall_end_01.nif -Decoding meshes\x\ex_hlaalu_buttress_05.nif -Decoding meshes\x\ex_hlaalu_wall_gate_03.nif -Decoding meshes\x\ex_hlaalu_wall_gate_01.nif -Decoding meshes\x\ex_hlaalu_buttress_03.nif -Decoding meshes\x\ex_hlaalu_wall_gate_04.nif -Decoding meshes\x\ex_hlaalu_wall_gate_02.nif -Decoding meshes\x\ex_hlaalu_buttress_01.nif -Decoding meshes\x\ex_hlaalu_wall_curve_01.nif -Decoding meshes\x\ex_hlaalu_striderport_01.nif -Decoding meshes\x\ex_holamayan_cover_01.nif -Decoding meshes\i\in_t_council_beams_02.nif -Decoding meshes\i\in_c_connect_stair_short.nif -Decoding meshes\i\in_redoran_ladder_01.nif -Decoding meshes\i\in_redoran_ashpit_02.nif -Decoding meshes\i\in_redoran_s_3way_01.nif -Decoding meshes\i\in_redoran_s_4way_01.nif -Decoding meshes\i\in_redoran_tower_01.nif -Decoding meshes\i\in_redoran_l_join_01.nif -Decoding meshes\i\in_redoran_s_hall_01.nif -Decoding meshes\i\in_redoran_window_01.nif -Decoding meshes\i\in_redoran_tavern_01.nif -Decoding meshes\i\in_redoran_l_hall_01.nif -Decoding meshes\i\in_redoran_steps_02.nif -Decoding meshes\i\in_redoran_steps_03.nif -Decoding meshes\i\in_redoran_l_cap_01.nif -Decoding meshes\i\in_redoran_l_3way_01.nif -Decoding meshes\i\in_redoran_l_4way_01.nif -Decoding meshes\i\in_redoran_s_hall_02.nif -Decoding meshes\i\in_redoran_s_cap_01.nif -Decoding meshes\i\in_redoran_ashpit_01.nif -Decoding meshes\i\in_redoran_barracks_01.nif -Decoding meshes\i\in_redoran_window2_01.nif -Decoding meshes\i\in_redoran_hut_jamb_01.nif -Decoding meshes\i\in_redoran_s_corner_01.nif -Decoding meshes\i\in_redoran_l_corner_01.nif -Decoding meshes\i\in_redoran_woodrail_01.nif -Decoding meshes\i\in_t_doorjamb_hall_small.nif -Decoding meshes\i\in_redoran_staircase_03.nif -Decoding meshes\i\in_redoran_hut_bfloor_02.nif -Decoding meshes\i\in_redoran_hut_bfloor_01.nif -Decoding meshes\i\in_redoran_hut_tfloor_01.nif -Decoding meshes\i\in_redoran_l_doorjamb_01.nif -Decoding meshes\i\in_redoran_staircase_02.nif -Decoding meshes\i\in_redoran_staircase_04.nif -Decoding meshes\i\in_dagoth_scaffold00.nif -Decoding meshes\i\in_t_housepod_stairs.nif -Decoding meshes\i\in_t_housepod_01_hall.nif -Decoding meshes\i\in_t_housepod_pole_01.nif -Decoding meshes\i\in_t_housepod_pole_02.nif -Decoding meshes\i\in_t_housepod_pole_04.nif -Decoding meshes\i\in_t_housepod_pole_03.nif -Decoding meshes\i\in_t_housepod_2ndfloor.nif -Decoding meshes\i\in_t_housepod_2flr_stair.nif -Decoding meshes\i\in_t_housepod_01_hall_02.nif -Decoding meshes\i\in_t_housepod_djamb_exit.nif -Decoding meshes\i\in_velothismall_ws_01.nif -Decoding meshes\i\in_velothilarge_con_01.nif -Decoding meshes\i\in_velothismall_cap_02.nif -Decoding meshes\i\in_velothilarge_cap_01.nif -Decoding meshes\i\in_velothismall_pit_01.nif -Decoding meshes\i\in_velothismall_mid_01.nif -Decoding meshes\i\in_velothi_platform_01.nif -Decoding meshes\i\in_velothismall_dj_01.nif -Decoding meshes\i\in_velothismall_cap_01.nif -Decoding meshes\i\in_velothismall_pit_02.nif -Decoding meshes\i\in_velothismall_pitd_02.nif -Decoding meshes\i\in_velothismall_pitd_04.nif -Decoding meshes\i\in_velothismall_pitd_06.nif -Decoding meshes\i\in_velothi_s_pitstep_01.nif -Decoding meshes\i\in_velothi_s_stairsl_01.nif -Decoding meshes\i\in_velothismall_hall_04.nif -Decoding meshes\i\in_velothismall_rail_01.nif -Decoding meshes\i\in_velothismall_ramp_01.nif -Decoding meshes\i\in_velothismall_ramp_03.nif -Decoding meshes\i\in_velothismall_room_11.nif -Decoding meshes\i\in_velothismall_r8_ramp.nif -Decoding meshes\i\in_velothilarge_ramp_01.nif -Decoding meshes\i\in_velothismall_room_08.nif -Decoding meshes\i\in_velothismall_room_02.nif -Decoding meshes\i\in_velothismall_room_06.nif -Decoding meshes\i\in_velothismall_room_04.nif -Decoding meshes\i\in_velothismall_4way_01.nif -Decoding meshes\i\in_velothismall_3way_01.nif -Decoding meshes\i\in_velothi_s_ceiling_01.nif -Decoding meshes\i\in_velothi_s_ramplong_01.nif -Decoding meshes\i\in_velothi_s_ramplong_02.nif -Decoding meshes\i\in_velothismall_wall_03.nif -Decoding meshes\i\in_velothismall_wall_01.nif -Decoding meshes\i\in_velothi_s_doorjam_01.nif -Decoding meshes\i\in_velothismall_pitd_01.nif -Decoding meshes\i\in_velothismall_pitd_03.nif -Decoding meshes\i\in_velothismall_pitd_05.nif -Decoding meshes\i\in_velothi_s_pitstep_02.nif -Decoding meshes\i\in_velothismall_hall_01.nif -Decoding meshes\i\in_velothismall_hall_03.nif -Decoding meshes\i\in_velothismall_ramp_02.nif -Decoding meshes\i\in_velothilarge_4way_01.nif -Decoding meshes\i\in_velothilarge_3way_01.nif -Decoding meshes\i\in_velothismall_room_10.nif -Decoding meshes\i\in_velothismall_room_12.nif -Decoding meshes\i\in_velothismall_door_01.nif -Decoding meshes\i\in_velothilarge_hall_01.nif -Decoding meshes\i\in_velothilarge_ramp_02.nif -Decoding meshes\i\in_velothismall_room_09.nif -Decoding meshes\i\in_velothismall_room_03.nif -Decoding meshes\i\in_velothismall_room_01.nif -Decoding meshes\i\in_velothismall_room_07.nif -Decoding meshes\i\in_velothismall_room_05.nif -Decoding meshes\i\in_velothismall_3way_02.nif -Decoding meshes\i\in_velothismall_dome_01.nif -Decoding meshes\i\in_velothismall_curve_02.nif -Decoding meshes\i\in_velothismall_curve_01.nif -Decoding meshes\i\in_velothismall_wall_02.nif -Decoding meshes\i\in_velothismall_r8_dome.nif -Decoding meshes\i\in_velothi_s_halfhall_01.nif -Decoding meshes\i\in_stronghold_hall03.nif -Decoding meshes\i\in_stronghold_hall00.nif -Decoding meshes\i\in_stronghold_wall00.nif -Decoding meshes\i\in_stronghold_wall10.nif -Decoding meshes\i\in_stronghold_dome00.nif -Decoding meshes\i\in_stronghold_arch00.nif -Decoding meshes\i\in_stronghold_hall04.nif -Decoding meshes\i\in_strong_balcony10.nif -Decoding meshes\i\in_strong_balcony00.nif -Decoding meshes\i\in_strong_archway00.nif -Decoding meshes\i\in_strong_archway01.nif -Decoding meshes\i\in_strong_shutter00.nif -Decoding meshes\i\in_strongruin_hall02.nif -Decoding meshes\i\in_strong_doorjam00.nif -Decoding meshes\i\in_strong_hallpill00.nif -Decoding meshes\i\in_strongruin_hall01.nif -Decoding meshes\i\in_stronghold_hall02.nif -Decoding meshes\i\in_strong_dualpill00.nif -Decoding meshes\i\in_stronghold_hall01.nif -Decoding meshes\i\in_strongruin_hall00.nif -Decoding meshes\i\in_strongruin_wall10.nif -Decoding meshes\i\in_strongruin_wall00.nif -Decoding meshes\i\in_strongruin2_hall02.nif -Decoding meshes\i\in_stronghold_corr2_05.nif -Decoding meshes\i\in_stronghold_corr2_07.nif -Decoding meshes\i\in_stronghold_corr2_01.nif -Decoding meshes\i\in_stronghold_corr2_03.nif -Decoding meshes\i\in_stronghold_corr3_01.nif -Decoding meshes\i\in_stronghold_stairs00.nif -Decoding meshes\i\in_stronghold_hcorr00.nif -Decoding meshes\i\in_stronghold_hcorr02.nif -Decoding meshes\i\in_stronghold_lpill00.nif -Decoding meshes\i\in_strongruin_corr2_07.nif -Decoding meshes\i\in_strongruin_corr2_05.nif -Decoding meshes\i\in_strongruin_corr2_03.nif -Decoding meshes\i\in_strongruin_corr2_01.nif -Decoding meshes\i\in_strongruin_corr3_01.nif -Decoding meshes\i\in_strongruin_hcorr00.nif -Decoding meshes\i\in_stronghold_corr4_00.nif -Decoding meshes\i\in_stronghold_corr2_04.nif -Decoding meshes\i\in_stronghold_corr2_06.nif -Decoding meshes\i\in_stronghold_corr2_00.nif -Decoding meshes\i\in_stronghold_corr2_02.nif -Decoding meshes\i\in_strongruin2_ramp10.nif -Decoding meshes\i\in_stronghold_corr3_00.nif -Decoding meshes\i\in_strongruin_hcorr01.nif -Decoding meshes\i\in_strongruin_hcorr02.nif -Decoding meshes\i\in_strongruin2_hall01.nif -Decoding meshes\i\in_strongruin2_hall04.nif -Decoding meshes\i\in_strongruin2_hall03.nif -Decoding meshes\i\in_stronghold_hcorr01.nif -Decoding meshes\i\in_strongruin2_hall00.nif -Decoding meshes\i\in_strong_vaultdoor00.nif -Decoding meshes\i\in_strongruin_corr2_06.nif -Decoding meshes\i\in_strongruin_corr2_04.nif -Decoding meshes\i\in_strongruin_corr2_02.nif -Decoding meshes\i\in_strongruin_corr2_00.nif -Decoding meshes\i\in_strongruin_corr3_00.nif -Decoding meshes\i\in_strongruin_stairs00.nif -Decoding meshes\i\in_strongruin2_corr2_04.nif -Decoding meshes\i\in_strongruin2_corr2_02.nif -Decoding meshes\i\in_strongruin2_corr2_00.nif -Decoding meshes\i\in_strongruin2_corr3_02.nif -Decoding meshes\i\in_strongruin2_corr3_00.nif -Decoding meshes\i\in_strongruin2_corr1_00.nif -Decoding meshes\i\in_strongruin2_corr4_00.nif -Decoding meshes\i\in_stronghold_divider10.nif -Decoding meshes\i\in_strongruin_divider10.nif -Decoding meshes\i\in_strongruin2_shutter00.nif -Decoding meshes\i\in_strongruin_roofext00.nif -Decoding meshes\i\in_strongruin_hallpill00.nif -Decoding meshes\i\in_strongruin_collapse10.nif -Decoding meshes\i\in_strongruin2_corr2_03.nif -Decoding meshes\i\in_strongruin2_corr2_01.nif -Decoding meshes\i\in_strongruin2_corr3_01.nif -Decoding meshes\i\in_stronghold_divider00.nif -Decoding meshes\i\in_strong_portal_chamber.nif -Decoding meshes\i\in_stronghold_roofext00.nif -Decoding meshes\i\in_strongruin2_balcony00.nif -Decoding meshes\i\in_strongruin_divider00.nif -Decoding meshes\i\in_strongruin2_balcony10.nif -Decoding meshes\i\in_strongruin2_dualpill00.nif -Decoding meshes\i\in_strongruin2_hallpill00.nif -Decoding meshes\i\in_strongruin2_collapse00.nif -Decoding meshes\i\in_velothismall_pittw_b_01.nif -Decoding meshes\i\in_velothismall_pittd_b_01.nif -Decoding meshes\i\in_velothismall_pitct_b_01.nif -Decoding meshes\i\in_velothismall_pitcb_b_01.nif -Decoding meshes\i\in_velothismall_pitbd_b_01.nif -Decoding meshes\i\in_velothilarge_corner_01.nif -Decoding meshes\i\in_velothismall_pitmw_b_01.nif -Decoding meshes\i\in_velothismall_pittop_02.nif -Decoding meshes\i\in_velothismall_pittop_01.nif -Decoding meshes\i\in_velothismall_fresco_01.nif -Decoding meshes\i\in_velothismall_fresco_02.nif -Decoding meshes\i\in_velothismall_pitc_b_01.nif -Decoding meshes\i\in_velothismall_pitbot_02.nif -Decoding meshes\i\in_velothismall_pitbot_01.nif -Decoding meshes\i\in_velothismall_pitb_b_01.nif -Decoding meshes\i\in_velothismall_column_01.nif -Decoding meshes\i\in_velothismall_column_03.nif -Decoding meshes\i\in_velothismall_column_02.nif -Decoding meshes\i\in_velothilarge_stairs_01.nif -Decoding meshes\i\in_velothismall_pitd_b_02.nif -Decoding meshes\i\in_velothismall_pitd_b_03.nif -Decoding meshes\i\in_velothismall_pitd_b_01.nif -Decoding meshes\i\in_velothismall_corner_01.nif -Decoding meshes\i\in_velothismall_stairs_01.nif -Decoding meshes\i\in_velothismall_pitmid_02.nif -Decoding meshes\i\in_velothismall_pitmid_01.nif -Decoding meshes\i\in_t_monor01_cap_topright.nif -Decoding meshes\d\ex_v_palace_grate_02.nif -Decoding meshes\d\ex_v_palace_grate_01.nif -Decoding meshes\i\in_ashl_tent_banner_08.nif -Decoding meshes\i\in_ashl_tent_banner_04.nif -Decoding meshes\i\in_ashl_tent_banner_06.nif -Decoding meshes\i\in_ashl_tent_banner_02.nif -Decoding meshes\i\in_ashl_tent_banner_16.nif -Decoding meshes\i\in_ashl_tent_banner_14.nif -Decoding meshes\i\in_ashl_tent_banner_12.nif -Decoding meshes\i\in_ashl_tent_banner_10.nif -Decoding meshes\i\in_ashl_tent_banner_09.nif -Decoding meshes\i\in_ashl_tent_banner_05.nif -Decoding meshes\i\in_ashl_tent_banner_07.nif -Decoding meshes\i\in_ashl_tent_banner_01.nif -Decoding meshes\i\in_ashl_tent_banner_03.nif -Decoding meshes\i\in_ashl_tent_banner_15.nif -Decoding meshes\i\in_ashl_tent_banner_13.nif -Decoding meshes\i\in_ashl_tent_banner_11.nif -Decoding meshes\i\in_c_plain_room_side.nif -Decoding meshes\i\in_c_plain_room_center.nif -Decoding meshes\i\in_c_plain_room_corner.nif -Decoding meshes\i\in_c_plain_room_entry.nif -Decoding meshes\i\in_c_plain_hall_small.nif -Decoding meshes\i\in_c_plain_stair_short.nif -Decoding meshes\i\in_c_plain_r_cwin_bay_02.nif -Decoding meshes\i\in_c_plain_r_cwin_bay_01.nif -Decoding meshes\i\in_c_plain_r_swin_bay_01.nif -Decoding meshes\i\in_c_plain_room_cwin_02.nif -Decoding meshes\i\in_c_plain_r_cwin_rec_02.nif -Decoding meshes\i\in_c_plain_r_cwin_rec_03.nif -Decoding meshes\i\in_c_plain_r_cwin_rec_01.nif -Decoding meshes\i\in_c_plain_r_cwin_rec_04.nif -Decoding meshes\i\in_c_plain_r_swin_rec_01.nif -Decoding meshes\i\in_c_plain_r_cwin_tri_01.nif -Decoding meshes\i\in_c_plain_r_cwin_tri_02.nif -Decoding meshes\i\in_c_plain_r_swin_tri_01.nif -Decoding meshes\i\in_c_plain_room_swin_01.nif -Decoding meshes\i\in_c_plain_room_cwin_01.nif -Decoding meshes\d\hlaalu_loaddoor_ 02.nif -Decoding meshes\d\hlaalu_loaddoor_ 01.nif -Decoding meshes\i\in_common_lighthouse.nif -Decoding meshes\i\in_common_room_corner.nif -Decoding meshes\i\in_common_tower_thatch.nif -Decoding meshes\i\in_common_tower_thatch3.nif -Decoding meshes\i\in_common_tower_thatch2.nif -Decoding meshes\a\a_boot_hvy_leather_gnd.nif -Decoding meshes\i\in_c_djamb_rp_arched.nif -Decoding meshes\i\in_c_djamb_rich_arched.nif -Decoding meshes\i\in_c_djamb_plain_arched.nif -Decoding meshes\i\in_c_djamb_stone_square.nif -Decoding meshes\i\in_c_djamb_plain_square.nif -Decoding meshes\i\in_c_djamb_stone_arched.nif -Decoding meshes\menu_scroll_button_up.nif -Decoding meshes\menu_scroll_button_down.nif -Decoding meshes\menu_scroll_button_right.nif -Decoding meshes\menu_scroll_button_left.nif -Decoding meshes\c\c_belt_extravagant_1.nif -Decoding meshes\c\c_belt_extravagant_2.nif -Decoding meshes\i\in_c_thatch_room_pside.nif -Decoding meshes\i\in_c_thatch_room_pcorner.nif -Decoding meshes\i\in_c_thatch_room_pentry.nif -Decoding meshes\i\in_c_thatch_room_pcenter.nif -Decoding meshes\menu_small_energy_bar.nif -Decoding meshes\i\in_c_thatch_room_pcorner2.nif -Decoding meshes\i\in_c_thatch_room_pendside.nif -Decoding meshes\i\in_c_thatch_room_pendside2.nif -Decoding meshes\i\in_c_pillar_wood_tall.nif -Decoding meshes\i\in_c_rich_room_pside.nif -Decoding meshes\i\in_c_rich_room_side.nif -Decoding meshes\i\in_c_rich_room_entry.nif -Decoding meshes\i\in_c_rich_room_center.nif -Decoding meshes\i\in_c_rich_room_pcenter.nif -Decoding meshes\i\in_c_rich_room_pcorner.nif -Decoding meshes\i\in_c_rich_room_pentry.nif -Decoding meshes\i\in_c_rich_room_corner.nif -Decoding meshes\i\in_c_rich_r_swin_bay_01.nif -Decoding meshes\i\in_c_rich_r_cwin_bay_01.nif -Decoding meshes\i\in_c_rich_rp_cwin_bay_04.nif -Decoding meshes\i\in_c_rich_rp_cwin_bay_03.nif -Decoding meshes\i\in_c_rich_rp_cwin_bay_02.nif -Decoding meshes\i\in_c_rich_rp_cwin_bay_01.nif -Decoding meshes\i\in_c_rich_r_cwin_tri_02.nif -Decoding meshes\i\in_c_rich_r_cwin_rec_01.nif -Decoding meshes\i\in_c_rich_r_cwin_rec_03.nif -Decoding meshes\i\in_c_rich_r_swin_rec_01.nif -Decoding meshes\i\in_c_rich_room_pcorner2.nif -Decoding meshes\i\in_c_rich_r_cwin_bay_02.nif -Decoding meshes\i\in_c_rich_room_pendside2.nif -Decoding meshes\i\in_c_rich_r_swin_tri_01.nif -Decoding meshes\i\in_c_rich_r_cwin_tri_01.nif -Decoding meshes\i\in_c_rich_room_pendside.nif -Decoding meshes\i\in_c_rich_r_cwin_rec_02.nif -Decoding meshes\i\in_c_rich_r_cwin_rec_04.nif -Decoding meshes\x\ex_redwall_corner_02.nif -Decoding meshes\x\ex_redwall_corner_01.nif -Decoding meshes\x\ex_redwall_corner_03.nif -Decoding meshes\x\ex_redwall_straight_02.nif -Decoding meshes\x\ex_redwall_straight_01.nif -Decoding meshes\x\ex_dwrv_ruin_tower00.nif -Decoding meshes\x\ex_dwrv_steamstack00.nif -Decoding meshes\x\ex_dwrv_pipefitting00.nif -Decoding meshes\i\in_moldcave_sroom4_05.nif -Decoding meshes\i\in_moldcave_lroom3_02.nif -Decoding meshes\i\in_moldcave_lroom4_02.nif -Decoding meshes\i\in_moldcave_nat_exit00.nif -Decoding meshes\i\in_moldcave_sroom4_01.nif -Decoding meshes\i\in_moldcave_sroom3_01.nif -Decoding meshes\i\in_moldcave_lroom4_03.nif -Decoding meshes\i\in_moldcave_lroom4_01.nif -Decoding meshes\i\in_moldcave_lroom3_01.nif -Decoding meshes\i\in_moldcave_sroom4_04.nif -Decoding meshes\i\in_moldcave_lroom2_00.nif -Decoding meshes\i\in_moldcave_lroom3_00.nif -Decoding meshes\i\in_moldcave_lroom4_05.nif -Decoding meshes\i\in_moldcave_sroom4_03.nif -Decoding meshes\i\in_moldcave_doorway00.nif -Decoding meshes\i\in_moldcave_sroom3_02.nif -Decoding meshes\i\in_moldcave_sroom4_02.nif -Decoding meshes\i\in_moldcave_sroom2_00.nif -Decoding meshes\i\in_moldcave_sroom3_00.nif -Decoding meshes\i\in_moldcave_lroom4_04.nif -Decoding meshes\i\in_nord_ladder_01_a.nif -Decoding meshes\i\in_nord_fireplace_01.nif -Decoding meshes\i\in_nord_rafterboards_01.nif -Decoding meshes\x\ex_t_stair_90_l_short.nif -Decoding meshes\x\ex_t_stair_90_r_short.nif -Decoding meshes\i\in_bonecave_sroom4_05.nif -Decoding meshes\i\in_bonecave_lroom3_02.nif -Decoding meshes\i\in_bonecave_lroom4_02.nif -Decoding meshes\i\in_bonecave_sroom4_01.nif -Decoding meshes\i\in_bonecave_sroom3_01.nif -Decoding meshes\i\in_bonecave_lroom4_03.nif -Decoding meshes\i\in_bonecave_lroom4_01.nif -Decoding meshes\i\in_bonecave_lroom3_01.nif -Decoding meshes\i\in_bonecave_sroom4_04.nif -Decoding meshes\i\in_bonecave_lroom2_00.nif -Decoding meshes\i\in_bonecave_lroom3_00.nif -Decoding meshes\i\in_bonecave_lroom4_05.nif -Decoding meshes\i\in_bonecave_sroom4_03.nif -Decoding meshes\i\in_bonecave_doorway00.nif -Decoding meshes\i\in_bonecave_sroom3_02.nif -Decoding meshes\i\in_bonecave_sroom4_02.nif -Decoding meshes\i\in_bonecave_sroom2_00.nif -Decoding meshes\i\in_bonecave_sroom3_00.nif -Decoding meshes\i\in_bonecave_lroom4_04.nif -Decoding meshes\x\in_c_stair_plain_tall_01.nif -Decoding meshes\x\in_c_stair_plain_tall_02.nif -Decoding meshes\i\in_sewer_collapse00.nif -Decoding meshes\i\in_sewer_anchorlock00.nif -Decoding meshes\i\in_vivec_waterspout_01.nif -Decoding meshes\i\in_cavern_wallmount00.nif -Decoding meshes\x\ex_de_shack_plank_03.nif -Decoding meshes\x\ex_de_shack_plank_04.nif -Decoding meshes\x\ex_de_shack_plank_02.nif -Decoding meshes\x\ex_de_ship_oarright.nif -Decoding meshes\x\ex_de_shack_plank_01.nif -Decoding meshes\x\ex_de_shack_steps_01.nif -Decoding meshes\x\ex_de_shack_awning_01.nif -Decoding meshes\x\ex_de_shack_awning_06.nif -Decoding meshes\x\ex_de_shack_awning_03.nif -Decoding meshes\x\ex_de_shack_awning_04.nif -Decoding meshes\x\ex_de_shack_awning_02.nif -Decoding meshes\x\ex_de_shack_awning_05.nif -Decoding meshes\d\in_redoran_hut_door_01.nif -Decoding meshes\d\in_t_door_small_load.nif -Decoding meshes\d\in_c_door_wood_square.nif -Decoding meshes\d\in_t_housepod_door_exit.nif -Decoding meshes\d\in_t_housepod_djamb_exit.nif -Decoding meshes\d\in_velothismall_ndoor_01.nif -Decoding meshes\x\ex_drystonewall_d_01.nif -Decoding meshes\x\ex_drystonewall_c_01.nif -Decoding meshes\x\ex_drystonewall_s_01.nif -Decoding meshes\x\ex_drystonewall_end_01.nif -Decoding meshes\d\in_strong_vaultdoor00.nif -Decoding meshes\d\ex_redoran_hut_01_a.nif -Decoding meshes\d\ex_t_door_sphere_01.nif -Decoding meshes\d\ex_t_door_slavepod_01.nif -Decoding meshes\d\ex_t_door_stone_large.nif -Decoding meshes\d\ex_redoran_barracks_01_a.nif -Decoding meshes\d\ex_velothi_loaddoor_02.nif -Decoding meshes\d\ex_velothi_loaddoor_01.nif -Decoding meshes\d\ex_velothi_entrance_03_a.nif -Decoding meshes\d\ex_velothi_entrance_01_a.nif -Decoding meshes\x\ex_gnisis_roadmarker_01.nif -Decoding meshes\d\in_redoran_barrackdoor_01.nif -Decoding meshes\i\in_mudcave_sroom4_05.nif -Decoding meshes\i\in_mudcave_lroom3_02.nif -Decoding meshes\i\in_mudcave_lroom4_02.nif -Decoding meshes\i\in_mudcave_sroom4_01.nif -Decoding meshes\i\in_mudcave_sroom3_01.nif -Decoding meshes\i\in_mudcave_lroom4_03.nif -Decoding meshes\i\in_mudcave_lroom4_01.nif -Decoding meshes\i\in_mudcave_lroom3_01.nif -Decoding meshes\i\in_mudcave_sroom4_04.nif -Decoding meshes\i\in_mudcave_lroom2_00.nif -Decoding meshes\i\in_mudcave_lroom3_00.nif -Decoding meshes\i\in_mudcave_lroom4_05.nif -Decoding meshes\i\in_mudcave_sroom4_03.nif -Decoding meshes\i\in_mudcave_doorway00.nif -Decoding meshes\i\in_mudcave_sroom3_02.nif -Decoding meshes\i\in_mudcave_sroom4_02.nif -Decoding meshes\i\in_mudcave_sroom2_00.nif -Decoding meshes\i\in_mudcave_sroom3_00.nif -Decoding meshes\i\in_mudcave_lroom4_04.nif -Decoding meshes\i\in_mudcave_nat_exit00.nif -Decoding meshes\x\ex_terrain_woodstep_01.nif -Decoding meshes\x\ex_t_bridge_lcurved.nif -Decoding meshes\n\ingred_dreugh_wax_01.nif -Decoding meshes\n\ingred_ectoplasm_01.nif -Decoding meshes\n\ingred_fire_salts_01.nif -Decoding meshes\n\ingred_whickwheat_01.nif -Decoding meshes\n\ingred_ash_salts_01.nif -Decoding meshes\n\ingred_hound_meat_01.nif -Decoding meshes\n\ingred_scathecraw_01.nif -Decoding meshes\n\ingred_sload_soap_01.nif -Decoding meshes\n\ingred_bc_spore_pod.nif -Decoding meshes\n\ingred_scamp_skin_01.nif -Decoding meshes\n\ingred_trama_root_01.nif -Decoding meshes\n\ingred_moon_sugar_01.nif -Decoding meshes\n\ingred_guar_hide_01.nif -Decoding meshes\n\ingred_alit_hide_01.nif -Decoding meshes\n\ingred_gold_kanet_01.nif -Decoding meshes\n\ingred_chokeweed_01.nif -Decoding meshes\n\ingred_void_salts_01.nif -Decoding meshes\n\ingred_blonemeal_01.nif -Decoding meshes\n\ingred_fire_petal_01.nif -Decoding meshes\n\ingred_raw_glass_01.nif -Decoding meshes\n\ingred_red_lichen_01.nif -Decoding meshes\n\ingred_gravedust_01.nif -Decoding meshes\n\ingred_hackle-lo_01.nif -Decoding meshes\n\ingred_frost_salts_01.nif -Decoding meshes\n\ingred_ghoul_heart_01.nif -Decoding meshes\n\ingred_green_lichen_01.nif -Decoding meshes\n\ingred_daedra_heart_01.nif -Decoding meshes\n\ingred_daedra_skin_01.nif -Decoding meshes\n\ingred_bc_coda_flower.nif -Decoding meshes\n\ingred_black_anther_01.nif -Decoding meshes\n\ingred_black_lichen_01.nif -Decoding meshes\n\ingred_bc_ampoule_pod.nif -Decoding meshes\n\ingred_bittergreen_01.nif -Decoding meshes\n\ingred_bc_hypha_facia.nif -Decoding meshes\n\ingred_marshmerrow_01.nif -Decoding meshes\n\ingred_kresh_fiber_01.nif -Decoding meshes\n\ingred_kagouti_hide_01.nif -Decoding meshes\n\ingred_kwama_cuttle_01.nif -Decoding meshes\n\ingred_vampire_dust_01.nif -Decoding meshes\n\ingred_racer_plumes_01.nif -Decoding meshes\n\ingred_shalk_resin_01.nif -Decoding meshes\n\ingred_scrib_jelly_01.nif -Decoding meshes\n\ingred_scrib_jerky_01.nif -Decoding meshes\n\ingred_scrap_metal_01.nif -Decoding meshes\n\ingred_stoneflower_01.nif -Decoding meshes\n\ingred_6th_corpusmeat_01.nif -Decoding meshes\n\ingred_6th_corpusmeat_02.nif -Decoding meshes\n\ingred_6th_corpusmeat_03.nif -Decoding meshes\n\ingred_6th_corpusmeat_04.nif -Decoding meshes\n\ingred_6th_corpusmeat_05.nif -Decoding meshes\n\ingred_6th_corpusmeat_06.nif -Decoding meshes\n\ingred_6th_corpusmeat_07.nif -Decoding meshes\n\ingred_willow_anther_01.nif -Decoding meshes\n\ingred_corkbulb_root_01.nif -Decoding meshes\n\ingred_bc_bungler's_bane.nif -Decoding meshes\n\ingred_netch_leather_01.nif -Decoding meshes\d\ex_common_door_balcony.nif -Decoding meshes\n\ingred_corprus_weeping_01.nif -Decoding meshes\x\ex_gg_portcullis_01.nif -Decoding meshes\a\a_iron_gauntlet_gnd.nif -Decoding meshes\a\a_iron_pauldron_gnd.nif -Decoding meshes\d\in_de_llshipdoor_large.nif -Decoding meshes\i\in_hlaalu_room_door3.nif -Decoding meshes\i\in_hlaalu_hall_rail.nif -Decoding meshes\i\in_hlaalu_room_post.nif -Decoding meshes\i\in_hlaalu_room_side.nif -Decoding meshes\i\in_hlaalu_roomt_post.nif -Decoding meshes\i\in_hlaalu_hallt_end.nif -Decoding meshes\i\in_hlaalu_hall_3way.nif -Decoding meshes\i\in_hlaalu_room_rail.nif -Decoding meshes\i\in_hlaalu_hallt_3way.nif -Decoding meshes\i\in_hlaalu_hallt_4way.nif -Decoding meshes\i\in_hlaalu_roomt_side.nif -Decoding meshes\i\in_hlaalu_hall_4way.nif -Decoding meshes\i\in_hlaalu_room_door2.nif -Decoding meshes\i\in_hlaalu_room_entry.nif -Decoding meshes\i\in_hlaalu_room_door4.nif -Decoding meshes\i\in_hlaalu_room_door1.nif -Decoding meshes\i\in_hlaalu_ashpit_02.nif -Decoding meshes\i\in_hlaalu_ashpit_01.nif -Decoding meshes\i\in_hlaalu_hallway_ramp.nif -Decoding meshes\i\in_hlaalu_hallt_3wayd.nif -Decoding meshes\i\in_hlaalu_roomt_entry.nif -Decoding meshes\i\in_hlaalu_platform_02.nif -Decoding meshes\i\in_hlaalu_roomt_sided.nif -Decoding meshes\i\in_hlaalu_loaddoor_01.nif -Decoding meshes\i\in_hlaalu_hall_stairsr.nif -Decoding meshes\i\in_hlaalu_hall_stairsl.nif -Decoding meshes\i\in_hlaalu_loaddoor_02.nif -Decoding meshes\i\in_hlaalu_room_center.nif -Decoding meshes\i\in_hlaalu_hallway_end.nif -Decoding meshes\i\in_hlaalu_hallt_center.nif -Decoding meshes\i\in_hlaalu_room_stairsl.nif -Decoding meshes\i\in_hlaalu_room_stairsr.nif -Decoding meshes\i\in_hlaalu_platform_01.nif -Decoding meshes\i\in_hlaalu_hallt_entry.nif -Decoding meshes\i\in_hlaalu_room_corner.nif -Decoding meshes\i\in_hlaalu_roomt_center.nif -Decoding meshes\i\in_hlaalu_roomt_cornd_02.nif -Decoding meshes\i\in_hlaalu_roomt_cornd_01.nif -Decoding meshes\i\in_hlaalu_room_center_01.nif -Decoding meshes\i\in_hlaalu_hallway_stairs.nif -Decoding meshes\i\in_hlaalu_hallt_centerd.nif -Decoding meshes\i\in_hlaalu_hallway_center.nif -Decoding meshes\i\in_hlaalu_hall_corner_01.nif -Decoding meshes\i\in_hlaalu_hallt_cornd_02.nif -Decoding meshes\i\in_hlaalu_hallt_cornd_01.nif -Decoding meshes\i\in_hlaalu_doorjamb_load.nif -Decoding meshes\i\in_pycave_lroom3_01.nif -Decoding meshes\i\in_pycave_sroom3_01.nif -Decoding meshes\i\in_pycave_lroom3_00.nif -Decoding meshes\i\in_pycave_sroom3_00.nif -Decoding meshes\i\in_pycave_lroom3_02.nif -Decoding meshes\i\in_pycave_sroom3_02.nif -Decoding meshes\i\in_pycave_doorway00.nif -Decoding meshes\i\in_pycave_lroom2_00.nif -Decoding meshes\i\in_pycave_sroom2_00.nif -Decoding meshes\i\in_pycave_nat_exit00.nif -Decoding meshes\i\in_pycave_sroom4_04.nif -Decoding meshes\i\in_pycave_lroom4_04.nif -Decoding meshes\i\in_pycave_sroom4_05.nif -Decoding meshes\i\in_pycave_lroom4_05.nif -Decoding meshes\i\in_pycave_sroom4_01.nif -Decoding meshes\i\in_pycave_lroom4_01.nif -Decoding meshes\i\in_pycave_sroom4_02.nif -Decoding meshes\i\in_pycave_lroom4_02.nif -Decoding meshes\i\in_pycave_sroom4_03.nif -Decoding meshes\i\in_pycave_lroom4_03.nif -Decoding meshes\i\in_t_cave_conect_wiz.nif -Decoding meshes\i\in_t_cave_conect_plain.nif -Decoding meshes\i\in_t_cave_conect_endcap.nif -Decoding meshes\a\shield_netch_leather.nif -Decoding meshes\a\shield_nordic_leather.nif -Decoding meshes\i\in_t_manor01_leftcap.nif -Decoding meshes\i\in_t_manor02_backcap.nif -Decoding meshes\i\in_t_manor01_backcap.nif -Decoding meshes\i\in_t_manor_floor_01.nif -Decoding meshes\i\in_t_manor_stairs_01.nif -Decoding meshes\i\in_t_manor02_leftcap.nif -Decoding meshes\i\in_t_manor01_rightcap.nif -Decoding meshes\i\in_t_manor02_cap_right.nif -Decoding meshes\i\in_t_manor01_frontcap.nif -Decoding meshes\i\in_t_manor01_entry_top.nif -Decoding meshes\i\in_t_manor02_rightcap.nif -Decoding meshes\i\in_t_manor02_floor_01.nif -Decoding meshes\i\in_t_manor01_floor_01.nif -Decoding meshes\i\in_t_manor_djamb_exit.nif -Decoding meshes\i\in_t_manor01_cap_left.nif -Decoding meshes\i\in_t_manor02_cap_left.nif -Decoding meshes\i\in_t_manor02_entry_right.nif -Decoding meshes\i\in_t_manor01_entry_left.nif -Decoding meshes\i\in_t_manor02_entry_left.nif -Decoding meshes\i\in_t_railing_pole_rails.nif -Decoding meshes\i\in_lava_blacksquare.nif -Decoding meshes\i\in_lavacave_sroom4_05.nif -Decoding meshes\i\in_lavacave_lroom3_02.nif -Decoding meshes\i\in_lavacave_lroom4_02.nif -Decoding meshes\i\in_lavacave_nat_exit00.nif -Decoding meshes\i\in_lavacave_sroom4_01.nif -Decoding meshes\i\in_lavacave_sroom3_01.nif -Decoding meshes\i\in_lavacave_lroom4_03.nif -Decoding meshes\i\in_lavacave_lroom4_01.nif -Decoding meshes\i\in_lavacave_lroom3_01.nif -Decoding meshes\i\in_lavacave_sroom4_04.nif -Decoding meshes\i\in_lavacave_lroom2_00.nif -Decoding meshes\i\in_lavacave_lroom3_00.nif -Decoding meshes\i\in_lavacave_lroom4_05.nif -Decoding meshes\i\in_lavacave_sroom4_03.nif -Decoding meshes\i\in_lavacave_doorway00.nif -Decoding meshes\i\in_lavacave_sroom3_02.nif -Decoding meshes\i\in_lavacave_sroom4_02.nif -Decoding meshes\i\in_lavacave_sroom2_00.nif -Decoding meshes\i\in_lavacave_sroom3_00.nif -Decoding meshes\i\in_lavacave_lroom4_04.nif -Decoding meshes\i\in_t_manor02_entry_right01.nif -Decoding meshes\i\in_t_manor01_cap_topright.nif -Decoding meshes\i\in_hlaalu_roomt_corner_01.nif -Decoding meshes\i\in_hlaalu_roomt_corner_02.nif -Decoding meshes\i\in_hlaalu_roomt_corner_03.nif -Decoding meshes\i\in_hlaalu_hallt_corner_01.nif -Decoding meshes\r\xg_centurionspider.nif -Decoding meshes\x\ex_gg_fence_s_h_01.nif -Decoding meshes\x\ex_gg_particles_01.nif -Decoding meshes\x\ex_de_docks_3wayb.nif -Decoding meshes\x\ex_de_docks_4wayb.nif -Decoding meshes\x\ex_de_docks_3ways.nif -Decoding meshes\x\ex_de_shack_steps.nif -Decoding meshes\x\ex_de_docks_cleat.nif -Decoding meshes\x\ex_de_docks_center.nif -Decoding meshes\x\ex_de_ship_oarleft.nif -Decoding meshes\x\ex_de_scaffold_03.nif -Decoding meshes\x\ex_de_scaffold_02.nif -Decoding meshes\x\ex_de_scaffold_01.nif -Decoding meshes\x\ex_de_docks_3waysb.nif -Decoding meshes\x\ex_de_docks_steps.nif -Decoding meshes\x\ex_de_docks_piling.nif -Decoding meshes\x\ex_cave_stoneyb00.nif -Decoding meshes\x\ex_vivec_sbase_01.nif -Decoding meshes\x\ex_vivec_bridge_06.nif -Decoding meshes\x\ex_vivec_bridge_07.nif -Decoding meshes\x\ex_vivec_bridge_04.nif -Decoding meshes\x\ex_vivec_bridge_02.nif -Decoding meshes\x\ex_vivec_bridge_01.nif -Decoding meshes\x\ex_vivec_bt_tb_01.nif -Decoding meshes\x\ex_vivec_palace_01.nif -Decoding meshes\x\ex_vivec_ent_t_01.nif -Decoding meshes\x\ex_vivec_ent_t_02.nif -Decoding meshes\x\ex_vivec_ent_b_01.nif -Decoding meshes\x\ex_t_stair_spiral.nif -Decoding meshes\x\ex_t_stair_rcurve.nif -Decoding meshes\x\ex_t_stair_lcurve.nif -Decoding meshes\x\ex_v_sign_hallj_01.nif -Decoding meshes\x\ex_v_sign_arena_01.nif -Decoding meshes\x\ex_v_sign_hallw_01.nif -Decoding meshes\x\ex_dwrv_boulder20.nif -Decoding meshes\x\ex_dwrv_boulder30.nif -Decoding meshes\x\ex_dwrv_boulder00.nif -Decoding meshes\x\ex_dwrv_boulder10.nif -Decoding meshes\x\ex_nord_chimney_01.nif -Decoding meshes\x\in_c_doorframe_01.nif -Decoding meshes\x\ex_t_root_arch_01.nif -Decoding meshes\x\ex_imp_towerb_top.nif -Decoding meshes\x\ex_imp_statue_base.nif -Decoding meshes\x\ex_imp_mooring_01.nif -Decoding meshes\x\ex_imp_entrance_01.nif -Decoding meshes\x\ex_imp_wallent_01.nif -Decoding meshes\x\ex_imp_wallent_02.nif -Decoding meshes\x\ex_t_gateway_great.nif -Decoding meshes\x\ex_v_foundation_01.nif -Decoding meshes\x\ex_v_foundation_02.nif -Decoding meshes\x\ex_v_foundation_03.nif -Decoding meshes\x\ex_dae_b_bo_cape2.nif -Decoding meshes\x\ex_dae_b_bo_cape3.nif -Decoding meshes\x\ex_dae_b_bo_cape1.nif -Decoding meshes\x\ex_dae_wall_256_09.nif -Decoding meshes\x\ex_dae_wall_256_08.nif -Decoding meshes\x\ex_dae_wall_256_01.nif -Decoding meshes\x\ex_dae_wall_256_10.nif -Decoding meshes\x\ex_dae_wall_256_03.nif -Decoding meshes\x\ex_dae_wall_256_13.nif -Decoding meshes\x\ex_dae_wall_256_02.nif -Decoding meshes\x\ex_dae_wall_256_05.nif -Decoding meshes\x\ex_dae_wall_256_04.nif -Decoding meshes\x\ex_dae_wall_256_07.nif -Decoding meshes\x\ex_dae_wall_256_06.nif -Decoding meshes\x\ex_dae_wall_512_09.nif -Decoding meshes\x\ex_dae_wall_512_08.nif -Decoding meshes\x\ex_dae_wall_512_05.nif -Decoding meshes\x\ex_dae_wall_512_04.nif -Decoding meshes\x\ex_dae_wall_512_07.nif -Decoding meshes\x\ex_dae_wall_512_06.nif -Decoding meshes\x\ex_dae_wall_512_01.nif -Decoding meshes\x\ex_dae_wall_512_10.nif -Decoding meshes\x\ex_dae_wall_512_03.nif -Decoding meshes\x\ex_dae_wall_512_02.nif -Decoding meshes\x\ex_dae_azura_small.nif -Decoding meshes\x\ex_dae_sheogorath.nif -Decoding meshes\x\ex_dae_b_bo_ubody.nif -Decoding meshes\x\ex_dae_b_bo_lbody.nif -Decoding meshes\x\ex_redoran_hut_02.nif -Decoding meshes\x\ex_redoran_hut_01.nif -Decoding meshes\x\ex_redwall_post_01.nif -Decoding meshes\x\ex_redwall_arch_01.nif -Decoding meshes\x\ex_t_clawgrowth_02.nif -Decoding meshes\x\ex_t_clawgrowth_01.nif -Decoding meshes\x\ex_v_ban_vivec_01.nif -Decoding meshes\x\ex_v_ban_vivec_02.nif -Decoding meshes\x\ex_v_ban_arena_01.nif -Decoding meshes\x\ex_v_ban_faith_01.nif -Decoding meshes\x\ex_v_ban_stolms_01.nif -Decoding meshes\x\ex_v_ban_count_01.nif -Decoding meshes\x\ex_v_ban_hlaalu_01.nif -Decoding meshes\x\ex_v_ban_child_01.nif -Decoding meshes\x\ex_v_ban_speak_01.nif -Decoding meshes\x\ex_scave2_enter10.nif -Decoding meshes\x\ex_hlaalu_pole_01.nif -Decoding meshes\x\ex_hlaalu_pole_02.nif -Decoding meshes\x\ex_hlaalu_wall_01.nif -Decoding meshes\x\ex_hlaalu_wall_02.nif -Decoding meshes\x\ex_hlaalu_canal_05.nif -Decoding meshes\x\ex_hlaalu_canal_04.nif -Decoding meshes\x\ex_hlaalu_canal_07.nif -Decoding meshes\x\ex_hlaalu_canal_06.nif -Decoding meshes\x\ex_hlaalu_canal_11.nif -Decoding meshes\x\ex_hlaalu_canal_01.nif -Decoding meshes\x\ex_hlaalu_canal_10.nif -Decoding meshes\x\ex_hlaalu_canal_13.nif -Decoding meshes\x\ex_hlaalu_canal_03.nif -Decoding meshes\x\ex_hlaalu_canal_12.nif -Decoding meshes\x\ex_hlaalu_canal_02.nif -Decoding meshes\x\ex_hlaalu_canal_09.nif -Decoding meshes\x\ex_hlaalu_canal_08.nif -Decoding meshes\x\ex_hlaalu_steps_02.nif -Decoding meshes\x\ex_hlaalu_steps_03.nif -Decoding meshes\x\ex_hlaalu_steps_01.nif -Decoding meshes\x\ex_hlaalu_steps_06.nif -Decoding meshes\x\ex_hlaalu_steps_07.nif -Decoding meshes\x\ex_hlaalu_steps_04.nif -Decoding meshes\x\ex_hlaalu_steps_05.nif -Decoding meshes\x\ex_common_trellis.nif -Decoding meshes\x\ex_common_plat_lrg.nif -Decoding meshes\x\ex_common_plat_end.nif -Decoding meshes\x\ex_velothi_dome_01.nif -Decoding meshes\x\ex_siltstrider_01.nif -Decoding meshes\x\ex_siltstrider_02.nif -Decoding meshes\x\ex_siltstrider_03.nif -Decoding meshes\x\ex_siltstrider_04.nif -Decoding meshes\x\ex_siltstrider_05.nif -Decoding meshes\x\ex_ruin_boulder01.nif -Decoding meshes\x\ex_ruin_boulder00.nif -Decoding meshes\x\ex_ruin_boulder03.nif -Decoding meshes\x\ex_ruin_boulder02.nif -Decoding meshes\x\ex_ruin_boulder05.nif -Decoding meshes\x\ex_ruin_boulder04.nif -Decoding meshes\x\ex_ashl_banner_01.nif -Decoding meshes\x\ex_t_housestem_02.nif -Decoding meshes\x\ex_t_housestem_03.nif -Decoding meshes\x\ex_t_housestem_01.nif -Decoding meshes\x\ex_ashl_door_01.nif -Decoding meshes\x\ex_ashl_tent_03.nif -Decoding meshes\x\ex_ashl_tent_01.nif -Decoding meshes\x\ex_ashl_door_02.nif -Decoding meshes\x\ex_ashl_tent_04.nif -Decoding meshes\x\ex_ashl_tent_02.nif -Decoding meshes\x\ex_trellis_vine.nif -Decoding meshes\x\ex_dwrv_walker20.nif -Decoding meshes\x\ex_dwrv_walker10.nif -Decoding meshes\x\ex_dwrv_walker00.nif -Decoding meshes\x\ex_dwrv_observ00.nif -Decoding meshes\x\ex_dwrv_block10.nif -Decoding meshes\x\ex_dwrv_block20.nif -Decoding meshes\x\ex_dwrv_header00.nif -Decoding meshes\x\ex_dwrv_statue00.nif -Decoding meshes\x\ex_dwrv_enter00.nif -Decoding meshes\x\ex_dwrv_bridge10.nif -Decoding meshes\x\ex_dwrv_bridge00.nif -Decoding meshes\x\ex_dwrv_block00.nif -Decoding meshes\x\ex_dwrv_alcove10.nif -Decoding meshes\x\ex_dwrv_cosmo00.nif -Decoding meshes\x\ex_dwrv_block30.nif -Decoding meshes\x\ex_dwrv_alcove00.nif -Decoding meshes\x\ex_ruin_rubble20.nif -Decoding meshes\x\ex_ruin_rubble00.nif -Decoding meshes\x\ex_ruin_rubble10.nif -Decoding meshes\x\ex_dae_pillar_02.nif -Decoding meshes\x\ex_dae_pillar_03.nif -Decoding meshes\x\ex_dae_pillar_01.nif -Decoding meshes\x\ex_dae_b_bo_head.nif -Decoding meshes\x\ex_dae_boethiah.nif -Decoding meshes\x\ex_daed_ruin_01.nif -Decoding meshes\x\ex_dae_b_bo_axe.nif -Decoding meshes\x\ex_dae_molagbal.nif -Decoding meshes\x\ex_barnacles_01.nif -Decoding meshes\x\ex_barnacles_03.nif -Decoding meshes\x\ex_barnacles_05.nif -Decoding meshes\x\ex_barnacles_02.nif -Decoding meshes\x\ex_barnacles_04.nif -Decoding meshes\x\ex_barnacles_06.nif -Decoding meshes\x\ex_cave_grass00.nif -Decoding meshes\x\ex_cave_scrub00.nif -Decoding meshes\x\ex_gg_fence_s_04.nif -Decoding meshes\x\ex_gg_fence_s_03.nif -Decoding meshes\x\ex_gg_fence_s_02.nif -Decoding meshes\x\ex_gg_fence_s_01.nif -Decoding meshes\x\ex_redwall_up_01.nif -Decoding meshes\x\ex_redwall_b_01.nif -Decoding meshes\x\ex_redwall_b_03.nif -Decoding meshes\x\ex_redwall_b_05.nif -Decoding meshes\x\ex_redwall_p_01.nif -Decoding meshes\x\ex_redwall_p_03.nif -Decoding meshes\x\ex_redwall_p_05.nif -Decoding meshes\x\ex_redwall_b_02.nif -Decoding meshes\x\ex_redwall_b_04.nif -Decoding meshes\x\ex_redwall_b_06.nif -Decoding meshes\x\ex_redwall_p_02.nif -Decoding meshes\x\ex_redwall_p_04.nif -Decoding meshes\x\ex_de_constr_03.nif -Decoding meshes\x\ex_de_constr_01.nif -Decoding meshes\x\ex_de_constr_05.nif -Decoding meshes\x\ex_de_railing_01.nif -Decoding meshes\x\ex_de_railing_03.nif -Decoding meshes\x\ex_de_railing_02.nif -Decoding meshes\x\ex_de_railing_05.nif -Decoding meshes\x\ex_de_railing_04.nif -Decoding meshes\x\ex_de_railing_06.nif -Decoding meshes\x\ex_de_docks_128.nif -Decoding meshes\x\ex_de_shipwreck.nif -Decoding meshes\x\ex_de_docks_3way.nif -Decoding meshes\x\ex_de_docks_4way.nif -Decoding meshes\x\ex_de_docks_endc.nif -Decoding meshes\x\ex_de_docks_ends.nif -Decoding meshes\x\ex_de_constr_02.nif -Decoding meshes\x\ex_de_constr_06.nif -Decoding meshes\x\ex_de_constr_04.nif -Decoding meshes\x\ex_de_docks_end.nif -Decoding meshes\x\ex_de_docks_gate.nif -Decoding meshes\x\ex_de_shack_door.nif -Decoding meshes\x\ex_vivec_hfq_03.nif -Decoding meshes\x\ex_vivec_hfq_01.nif -Decoding meshes\x\ex_vivec_pqs_02.nif -Decoding meshes\x\ex_vivec_b_tb_01.nif -Decoding meshes\x\ex_vivec_b_wb_01.nif -Decoding meshes\x\ex_vivec_g_r_01.nif -Decoding meshes\x\ex_vivec_hfq_04.nif -Decoding meshes\x\ex_vivec_hfq_02.nif -Decoding meshes\x\ex_vivec_pqs_01.nif -Decoding meshes\x\ex_vivec_w_c_01.nif -Decoding meshes\x\ex_vivec_w_g_01.nif -Decoding meshes\x\ex_vivec_w_e_01.nif -Decoding meshes\x\ex_vivec_telt_01.nif -Decoding meshes\x\ex_vivec_g_r_02.nif -Decoding meshes\x\ex_vivec_b_t_01.nif -Decoding meshes\x\ex_ci_doorjam_01.nif -Decoding meshes\x\ex_volcano_steam.nif -Decoding meshes\x\ex_ropebridge_01.nif -Decoding meshes\x\ex_nord_rock_01.nif -Decoding meshes\x\ex_nord_well_01.nif -Decoding meshes\x\ex_nord_door_02.nif -Decoding meshes\x\ex_nord_rock_02.nif -Decoding meshes\x\ex_nord_house_03.nif -Decoding meshes\x\ex_nord_house_02.nif -Decoding meshes\x\ex_nord_house_01.nif -Decoding meshes\x\ex_nord_house_05.nif -Decoding meshes\x\ex_nord_house_04.nif -Decoding meshes\x\ex_nord_doorf_01.nif -Decoding meshes\x\ex_holamayan_01.nif -Decoding meshes\x\ex_emp_tower_01.nif -Decoding meshes\x\ex_imp_arrowslit.nif -Decoding meshes\x\ex_imp_bridge_01.nif -Decoding meshes\x\ex_imp_bridge_02.nif -Decoding meshes\x\ex_hlaalu_fb_01.nif -Decoding meshes\x\ex_hlaalu_win_03.nif -Decoding meshes\x\ex_hlaalu_win_02.nif -Decoding meshes\x\ex_hlaalu_win_01.nif -Decoding meshes\x\ex_hlaalu_bal_02.nif -Decoding meshes\x\ex_hlaalu_bal_01.nif -Decoding meshes\x\ex_hlaalu_fb_02.nif -Decoding meshes\x\ex_t_housepod_01.nif -Decoding meshes\x\ex_t_housepod_03.nif -Decoding meshes\x\ex_t_housepod_02.nif -Decoding meshes\x\ex_t_housepod_04.nif -Decoding meshes\x\ex_t_spiralramp.nif -Decoding meshes\x\ex_t_councilhall.nif -Decoding meshes\x\ex_t_pole_hooked.nif -Decoding meshes\x\ex_t_tower_bent.nif -Decoding meshes\x\ex_t_menhir_l_01.nif -Decoding meshes\x\ex_t_doorway_01.nif -Decoding meshes\x\ex_t_root_xl_03.nif -Decoding meshes\x\ex_t_root_xl_01.nif -Decoding meshes\x\ex_t_bigroot_01.nif -Decoding meshes\x\ex_t_slavemarket.nif -Decoding meshes\x\ex_t_slavepod_01.nif -Decoding meshes\x\ex_t_platform_01.nif -Decoding meshes\x\ex_t_platform_02.nif -Decoding meshes\x\ex_t_doorway_02.nif -Decoding meshes\x\ex_t_root_xl_02.nif -Decoding meshes\x\ex_t_bigroot_02.nif -Decoding meshes\x\ex_v_ban_walk_01.nif -Decoding meshes\x\ex_v_2_floor_01.nif -Decoding meshes\x\ex_v_sign_fq_01.nif -Decoding meshes\x\ex_v_1_corner_01.nif -Decoding meshes\x\ex_v_2_corner_01.nif -Decoding meshes\x\ex_v_ban_imp_01.nif -Decoding meshes\x\ex_v_stdeyln_01.nif -Decoding meshes\x\ex_v_ban_avs_01.nif -Decoding meshes\x\ex_v_2_stairs_01.nif -Decoding meshes\x\ex_t_turret_02.nif -Decoding meshes\x\ex_t_menhir_01.nif -Decoding meshes\x\ex_t_dormer_01.nif -Decoding meshes\x\ex_t_stair_01.nif -Decoding meshes\x\ex_t_root_stem.nif -Decoding meshes\x\ex_t_awning_02.nif -Decoding meshes\x\ex_t_manor_01.nif -Decoding meshes\x\ex_t_manor_02.nif -Decoding meshes\x\ex_t_dock_main.nif -Decoding meshes\x\ex_t_brace_01.nif -Decoding meshes\x\ex_t_turret_03.nif -Decoding meshes\x\ex_t_turret_01.nif -Decoding meshes\x\ex_t_dormer_02.nif -Decoding meshes\x\ex_t_awning_01.nif -Decoding meshes\x\ex_t_rootball.nif -Decoding meshes\x\ex_t_root_hook.nif -Decoding meshes\x\ex_vivec_h_02.nif -Decoding meshes\x\ex_vivec_h_12.nif -Decoding meshes\x\ex_vivec_c_02.nif -Decoding meshes\x\ex_vivec_g_02.nif -Decoding meshes\x\ex_vivec_w_02.nif -Decoding meshes\x\ex_v_banner_02.nif -Decoding meshes\x\ex_vivec_h_14.nif -Decoding meshes\x\ex_vivec_h_15.nif -Decoding meshes\x\ex_vivec_c_03.nif -Decoding meshes\x\ex_vivec_h_03.nif -Decoding meshes\x\ex_vivec_w_03.nif -Decoding meshes\x\ex_vivec_h_06.nif -Decoding meshes\x\ex_vivec_hf_02.nif -Decoding meshes\x\ex_vivec_hf_04.nif -Decoding meshes\x\ex_vivec_h_07.nif -Decoding meshes\x\ex_vivec_pq_02.nif -Decoding meshes\x\ex_vivec_pq_04.nif -Decoding meshes\x\ex_vivec_lp_02.nif -Decoding meshes\x\ex_vivec_h_05.nif -Decoding meshes\x\ex_vivec_h_17.nif -Decoding meshes\x\ex_vivec_h_16.nif -Decoding meshes\x\ex_vivec_t_04.nif -Decoding meshes\x\ex_vivec_h_04.nif -Decoding meshes\x\ex_vivec_c_04.nif -Decoding meshes\x\ex_v_bpost_01.nif -Decoding meshes\x\ex_v_bpost_03.nif -Decoding meshes\x\ex_v_stolms_01.nif -Decoding meshes\x\ex_v_bpost_02.nif -Decoding meshes\x\ex_vivec_h_09.nif -Decoding meshes\x\ex_v_banner_01.nif -Decoding meshes\x\ex_v_banner_03.nif -Decoding meshes\x\ex_vivec_w_01.nif -Decoding meshes\x\ex_vivec_t_01.nif -Decoding meshes\x\ex_vivec_h_11.nif -Decoding meshes\x\ex_vivec_h_01.nif -Decoding meshes\x\ex_vivec_g_01.nif -Decoding meshes\x\ex_vivec_b_01.nif -Decoding meshes\x\ex_vivec_c_01.nif -Decoding meshes\x\ex_vivec_h_10.nif -Decoding meshes\x\ex_vivec_h_13.nif -Decoding meshes\x\ex_v_1_wall_01.nif -Decoding meshes\x\ex_v_2_wall_01.nif -Decoding meshes\x\ex_vivec_hf_01.nif -Decoding meshes\x\ex_vivec_hf_03.nif -Decoding meshes\x\ex_vivec_pd_01.nif -Decoding meshes\x\ex_vivec_h_08.nif -Decoding meshes\x\ex_vivec_ps_01.nif -Decoding meshes\x\ex_vivec_pq_01.nif -Decoding meshes\x\ex_vivec_pq_03.nif -Decoding meshes\x\ex_vivec_lp_01.nif -Decoding meshes\x\ex_vivec_bt_01.nif -Decoding meshes\x\ex_rope_short.nif -Decoding meshes\x\ex_ship_plank.nif -Decoding meshes\x\ex_scrapwood04.nif -Decoding meshes\x\ex_scrapwood02.nif -Decoding meshes\x\ex_ship_stair.nif -Decoding meshes\x\ex_scrapwood05.nif -Decoding meshes\x\ex_scrapwood03.nif -Decoding meshes\x\ex_scrapwood01.nif -Decoding meshes\x\ex_dwrv_ruin50.nif -Decoding meshes\x\ex_dae_ruin_04.nif -Decoding meshes\x\ex_dae_ruin_02.nif -Decoding meshes\x\ex_dwrv_ruin20.nif -Decoding meshes\x\ex_de_shack_05.nif -Decoding meshes\x\ex_de_shack_03.nif -Decoding meshes\x\ex_de_shack_01.nif -Decoding meshes\x\ex_dwrv_wall20.nif -Decoding meshes\x\ex_dwrv_wall30.nif -Decoding meshes\x\ex_dwrv_wall50.nif -Decoding meshes\x\ex_dwrv_wall60.nif -Decoding meshes\x\ex_dwrv_wall40.nif -Decoding meshes\x\ex_dwrv_wall10.nif -Decoding meshes\x\ex_dae_claw_02.nif -Decoding meshes\x\ex_dwrv_ruin10.nif -Decoding meshes\x\ex_dwrv_pipe10.nif -Decoding meshes\x\ex_dwrv_ruin40.nif -Decoding meshes\x\ex_dwrv_ruin00.nif -Decoding meshes\x\ex_dae_ruin_01.nif -Decoding meshes\x\ex_dae_ruin_03.nif -Decoding meshes\x\ex_de_shack_04.nif -Decoding meshes\x\ex_de_shack_02.nif -Decoding meshes\x\ex_dwrv_wall00.nif -Decoding meshes\x\ex_de_sn_gate.nif -Decoding meshes\x\ex_dwrv_ruin30.nif -Decoding meshes\x\ex_dwrv_ruin60.nif -Decoding meshes\x\ex_dwrv_pipe00.nif -Decoding meshes\x\ex_dwrv_ruin80.nif -Decoding meshes\x\ex_dae_claw_01.nif -Decoding meshes\x\ex_de_rowboat.nif -Decoding meshes\x\ex_dwrv_ruin70.nif -Decoding meshes\x\ex_gg_fence_02.nif -Decoding meshes\x\ex_gg_fence_04.nif -Decoding meshes\x\ex_gg_pylon_01.nif -Decoding meshes\x\ex_gondola_01.nif -Decoding meshes\x\ex_gg_fence_03.nif -Decoding meshes\x\ex_gg_fence_01.nif -Decoding meshes\x\ex_gg_pylon_02.nif -Decoding meshes\x\ex_ashl_banner.nif -Decoding meshes\x\ex_coiled_rope.nif -Decoding meshes\x\ex_cave_sand00.nif -Decoding meshes\x\ex_cave_ash10.nif -Decoding meshes\x\ex_cave_ash00.nif -Decoding meshes\x\ex_lavaspark01.nif -Decoding meshes\x\ex_lavaspark03.nif -Decoding meshes\x\ex_longboat02.nif -Decoding meshes\x\ex_lavasteam00.nif -Decoding meshes\x\ex_lavaspark02.nif -Decoding meshes\x\ex_longboatwrk.nif -Decoding meshes\x\ex_longboat01.nif -Decoding meshes\x\ex_nord_win_01.nif -Decoding meshes\x\ex_nord_win_02.nif -Decoding meshes\x\ex_hlaalu_b_04.nif -Decoding meshes\x\ex_hlaalu_b_06.nif -Decoding meshes\x\ex_hlaalu_b_02.nif -Decoding meshes\x\ex_hlaalu_b_08.nif -Decoding meshes\x\ex_hlaalu_b_18.nif -Decoding meshes\x\ex_hlaalu_b_12.nif -Decoding meshes\x\ex_hlaalu_b_10.nif -Decoding meshes\x\ex_hlaalu_b_16.nif -Decoding meshes\x\ex_hlaalu_b_14.nif -Decoding meshes\x\ex_hlaalu_b_25.nif -Decoding meshes\x\ex_hlaalu_b_27.nif -Decoding meshes\x\ex_hlaalu_b_21.nif -Decoding meshes\x\ex_hlaalu_b_23.nif -Decoding meshes\x\ex_hlaalu_b_05.nif -Decoding meshes\x\ex_hlaalu_b_07.nif -Decoding meshes\x\ex_hlaalu_b_01.nif -Decoding meshes\x\ex_hlaalu_b_03.nif -Decoding meshes\x\ex_hlaalu_b_09.nif -Decoding meshes\x\ex_hlaalu_b_19.nif -Decoding meshes\x\ex_hlaalu_b_13.nif -Decoding meshes\x\ex_hlaalu_b_11.nif -Decoding meshes\x\ex_hlaalu_b_17.nif -Decoding meshes\x\ex_hlaalu_b_15.nif -Decoding meshes\x\ex_hlaalu_b_24.nif -Decoding meshes\x\ex_hlaalu_b_26.nif -Decoding meshes\x\ex_hlaalu_b_20.nif -Decoding meshes\x\ex_hlaalu_b_22.nif -Decoding meshes\x\ex_hlaalu_b_28.nif -Decoding meshes\x\ex_imp_wall_01.nif -Decoding meshes\x\ex_imp_keep_02.nif -Decoding meshes\x\ex_imp_plat_01.nif -Decoding meshes\x\ex_imp_pool_01.nif -Decoding meshes\x\ex_imp_dock_01.nif -Decoding meshes\x\ex_imp_keep_01.nif -Decoding meshes\x\ex_imp_dock_02.nif -Decoding meshes\x\ex_t_root_02.nif -Decoding meshes\x\ex_t_root_04.nif -Decoding meshes\x\ex_dae_azura.nif -Decoding meshes\x\ex_t_root_05.nif -Decoding meshes\x\ex_gg_ent_01.nif -Decoding meshes\x\ex_t_root_06.nif -Decoding meshes\x\ex_t_tunnel.nif -Decoding meshes\x\ex_t_root_01.nif -Decoding meshes\x\ex_t_pole_01.nif -Decoding meshes\x\ex_longboat.nif -Decoding meshes\x\ex_imp_plaza.nif -Decoding meshes\x\ex_t_root_03.nif -Decoding meshes\x\ex_cave_mt00.nif -Decoding meshes\x\ex_v_dock_01.nif -Decoding meshes\x\ex_t_dock_01.nif -Decoding meshes\f\ex_ashl_e_banner_r.nif -Decoding meshes\f\ex_ashl_a_banner_r.nif -Decoding meshes\f\ex_ashl_z_banner_r.nif -Decoding meshes\f\ex_ashl_u_banner_r.nif -Decoding meshes\f\ex_ashl_z_banner.nif -Decoding meshes\f\ex_ashl_a_banner.nif -Decoding meshes\f\ex_ashl_u_banner.nif -Decoding meshes\f\ex_ashl_e_banner.nif -Decoding meshes\f\ex_ashl_banner.nif -Decoding meshes\f\ex_boulder02.nif -Decoding meshes\f\ex_boulder04.nif -Decoding meshes\f\ex_boulder00.nif -Decoding meshes\f\ex_boulder05.nif -Decoding meshes\f\ex_boulder08.nif -Decoding meshes\f\ex_boulder07.nif -Decoding meshes\f\ex_boulder06.nif -Decoding meshes\f\ex_boulder03.nif -Decoding meshes\f\ex_boulder01.nif -Decoding meshes\d\ex_vivec_grate_01.nif -Decoding meshes\d\ex_emp_tower_01_b.nif -Decoding meshes\d\ex_emp_tower_01_a.nif -Decoding meshes\d\ex_imp_loaddoor_03.nif -Decoding meshes\d\ex_imp_loaddoor_02.nif -Decoding meshes\d\ex_imp_loaddoor_01.nif -Decoding meshes\d\in_t_s_plain_door.nif -Decoding meshes\d\in_v_s_trapdoor_02.nif -Decoding meshes\d\in_v_s_trapdoor_01.nif -Decoding meshes\d\in_v_s_jaildoor_01.nif -Decoding meshes\d\ex_v_cantondoor_01.nif -Decoding meshes\d\in_vivec_grate_01.nif -Decoding meshes\d\ex_common_door_01.nif -Decoding meshes\d\in_de_cabindoor.nif -Decoding meshes\d\in_t_door_small.nif -Decoding meshes\d\in_r_trapdoor_01.nif -Decoding meshes\d\in_c_door_arched.nif -Decoding meshes\d\in_h_trapdoor_01.nif -Decoding meshes\d\in_t_l_door_01.nif -Decoding meshes\d\in_r_s_door_01.nif -Decoding meshes\d\in_dae_door_01.nif -Decoding meshes\d\in_ar_door_01.nif -Decoding meshes\d\in_ci_door_01.nif -Decoding meshes\d\in_hlaalu_door.nif -Decoding meshes\d\ex_cave_door_01.nif -Decoding meshes\d\ex_de_ship_door.nif -Decoding meshes\d\ex_nord_door_01.nif -Decoding meshes\d\ex_nord_door_02.nif -Decoding meshes\d\ex_r_trapdoor_01.nif -Decoding meshes\d\ex_h_trapdoor_01.nif -Decoding meshes\d\ex_t_door_02.nif -Decoding meshes\d\ex_t_door_01.nif -Decoding meshes\l\light_6th_candle_05.nif -Decoding meshes\l\light_6th_candle_04.nif -Decoding meshes\l\light_6th_candle_06.nif -Decoding meshes\l\light_6th_candle_01.nif -Decoding meshes\l\light_6th_candle_03.nif -Decoding meshes\l\light_6th_candle_02.nif -Decoding meshes\base_anim_female.1st.nif -Decoding meshes\base_anim_female.nif -Decoding meshes\base_animkna.1st.nif -Decoding meshes\base_anim.1st.nif -Decoding meshes\anim_dancinggirl.nif -Decoding meshes\x\ex_common_tower_thatchedroof.nif -Decoding meshes\menu_scroll_hort_bar.nif -Decoding meshes\menu_scroll_vert_bar.nif -Decoding meshes\menu_scroll_scroller.nif -Decoding meshes\menu_rightbuttondown.nif -Decoding meshes\main_menu_button.nif -Decoding meshes\menu_button_frame.nif -Decoding meshes\menu_thick_border.nif -Decoding meshes\menu_rightbuttonup.nif -Decoding meshes\menu_scroll_slider.nif -Decoding meshes\menu_messagebox.nif -Decoding meshes\menu_icon_frame.nif -Decoding meshes\menu_head_block.nif -Decoding meshes\menu_thin_border.nif -Decoding meshes\menu_scroll_bar.nif -Decoding meshes\menu_contents.nif -Decoding meshes\menu_scroll_da.nif -Decoding meshes\menu_scroll_ua.nif -Decoding meshes\i\in_vs_pitfloor_01.nif -Decoding meshes\i\in_ar_shelltop_01.nif -Decoding meshes\i\in_ar_platform_10.nif -Decoding meshes\i\in_ar_platform_04.nif -Decoding meshes\i\in_ar_platform_05.nif -Decoding meshes\i\in_ar_platform_06.nif -Decoding meshes\i\in_ar_platform_07.nif -Decoding meshes\i\in_ar_platform_01.nif -Decoding meshes\i\in_ar_platform_02.nif -Decoding meshes\i\in_ar_platform_03.nif -Decoding meshes\i\in_ar_platform_08.nif -Decoding meshes\i\in_ar_platform_09.nif -Decoding meshes\i\in_hlaalu_hall_128.nif -Decoding meshes\i\in_hlaalu_door_01.nif -Decoding meshes\i\in_hlaalu_doorjamb.nif -Decoding meshes\i\in_t_council_beams.nif -Decoding meshes\i\in_t_balconyscreen.nif -Decoding meshes\i\in_dae_doorjamb_01.nif -Decoding meshes\i\in_mudcave2_s_end.nif -Decoding meshes\i\in_mudcave_stal10.nif -Decoding meshes\i\in_mudcave_stal00.nif -Decoding meshes\i\in_mudcave_stal30.nif -Decoding meshes\i\in_mudcave_stal20.nif -Decoding meshes\i\in_mudcave_stal50.nif -Decoding meshes\i\in_mudcave_stal40.nif -Decoding meshes\i\in_mudcave_exit00.nif -Decoding meshes\i\in_mudcave_form00.nif -Decoding meshes\i\in_mudcave_form10.nif -Decoding meshes\i\in_mudcave_form20.nif -Decoding meshes\i\in_mudcave_form30.nif -Decoding meshes\i\in_redoran_hut_01.nif -Decoding meshes\i\in_c_doorframe_01.nif -Decoding meshes\i\in_dagoth_bridge00.nif -Decoding meshes\i\in_dagoth_plate10.nif -Decoding meshes\i\in_dagoth_plate00.nif -Decoding meshes\i\in_dagoth_plate20.nif -Decoding meshes\i\in_r_guild_mage_01.nif -Decoding meshes\i\in_com_traptop_01.nif -Decoding meshes\i\in_com_wincover_02.nif -Decoding meshes\i\in_com_wincover_01.nif -Decoding meshes\i\in_vsl_pitbd_b_01.nif -Decoding meshes\i\in_vsl_pitmd_b_01.nif -Decoding meshes\i\in_moldcave2_s_00.nif -Decoding meshes\i\in_moldcave2_s_01.nif -Decoding meshes\i\in_moldcave2_s_02.nif -Decoding meshes\i\in_moldcave2_s_03.nif -Decoding meshes\i\in_moldcave2_s_04.nif -Decoding meshes\i\in_moldcave2_s_05.nif -Decoding meshes\i\in_moldcave2_s_06.nif -Decoding meshes\i\in_moldcave_exit00.nif -Decoding meshes\i\in_moldcave2_s_end.nif -Decoding meshes\i\in_moldcave4_s_00.nif -Decoding meshes\i\in_moldcave_stal00.nif -Decoding meshes\i\in_moldcave_stal10.nif -Decoding meshes\i\in_moldcave_stal20.nif -Decoding meshes\i\in_moldcave_stal30.nif -Decoding meshes\i\in_moldcave_stal40.nif -Decoding meshes\i\in_moldcave_stal50.nif -Decoding meshes\i\in_moldcave_form20.nif -Decoding meshes\i\in_moldcave_form30.nif -Decoding meshes\i\in_moldcave_form00.nif -Decoding meshes\i\in_moldcave_form10.nif -Decoding meshes\i\in_velothicave_01.nif -Decoding meshes\i\in_velothicave_03.nif -Decoding meshes\i\in_velothicave_02.nif -Decoding meshes\i\in_velothicave_04.nif -Decoding meshes\i\in_t_l_room_corner.nif -Decoding meshes\i\in_t_l_room_entry.nif -Decoding meshes\i\in_t_l_hall_corner.nif -Decoding meshes\i\in_t_l_room_floor.nif -Decoding meshes\i\in_t_l_doorjamb_01.nif -Decoding meshes\i\in_v_l_int_wall_01.nif -Decoding meshes\i\in_v_l_int_wall_02.nif -Decoding meshes\i\in_v_l_int_rail_01.nif -Decoding meshes\i\in_r_l_int_wall_01.nif -Decoding meshes\i\in_r_l_int_wall_02.nif -Decoding meshes\i\in_r_l_doorjamb_02.nif -Decoding meshes\i\in_r_l_int_rail_01.nif -Decoding meshes\i\in_r_l_int_arch_01.nif -Decoding meshes\i\in_bonecave2_s_08.nif -Decoding meshes\i\in_bonecave2_s_09.nif -Decoding meshes\i\in_bonecave2_s_00.nif -Decoding meshes\i\in_bonecave2_s_01.nif -Decoding meshes\i\in_bonecave2_s_02.nif -Decoding meshes\i\in_bonecave2_s_03.nif -Decoding meshes\i\in_bonecave2_s_04.nif -Decoding meshes\i\in_bonecave2_s_05.nif -Decoding meshes\i\in_bonecave2_s_06.nif -Decoding meshes\i\in_bonecave2_s_07.nif -Decoding meshes\i\in_bonecave2_s_end.nif -Decoding meshes\i\in_bonecave4_s_00.nif -Decoding meshes\i\in_bonecave_stal00.nif -Decoding meshes\i\in_bonecave_stal10.nif -Decoding meshes\i\in_bonecave_stal20.nif -Decoding meshes\i\in_bonecave_stal30.nif -Decoding meshes\i\in_bonecave_stal40.nif -Decoding meshes\i\in_bonecave_stal50.nif -Decoding meshes\i\in_bonecave_form20.nif -Decoding meshes\i\in_bonecave_form30.nif -Decoding meshes\i\in_bonecave_form00.nif -Decoding meshes\i\in_bonecave_form10.nif -Decoding meshes\i\in_bonecave_exit00.nif -Decoding meshes\i\in_impbig_4way_01.nif -Decoding meshes\i\in_impbig_wall_01.nif -Decoding meshes\i\in_impbig_blend_01.nif -Decoding meshes\i\in_c_plain_corner.nif -Decoding meshes\i\in_c_pillar_stone.nif -Decoding meshes\i\in_c_plain_endcap.nif -Decoding meshes\i\in_t_s_room_corner.nif -Decoding meshes\i\in_t_stairs_wiz_01.nif -Decoding meshes\i\in_t_stairs_wiz_03.nif -Decoding meshes\i\in_t_stairs_wiz_02.nif -Decoding meshes\i\in_t_s_room_entry.nif -Decoding meshes\i\in_t_s_shaft_6way.nif -Decoding meshes\i\in_t_s_djamb_plain.nif -Decoding meshes\i\in_t_s_room_center.nif -Decoding meshes\i\in_v_s_int_wall_01.nif -Decoding meshes\i\in_v_s_int_wall_03.nif -Decoding meshes\i\in_v_s_int_wall_02.nif -Decoding meshes\i\in_v_s_int_wall_04.nif -Decoding meshes\i\in_v_s_domeroom_01.nif -Decoding meshes\i\in_r_s_int_wall_01.nif -Decoding meshes\i\in_r_s_int_wall_02.nif -Decoding meshes\i\in_r_s_doorjamb_01.nif -Decoding meshes\i\in_r_s_int_rail_01.nif -Decoding meshes\i\in_r_s_int_rail_02.nif -Decoding meshes\i\in_r_s_int_arch_01.nif -Decoding meshes\i\in_r_s_int_arch_02.nif -Decoding meshes\i\in_c_stone_corner.nif -Decoding meshes\i\in_c_stone_endcap.nif -Decoding meshes\i\in_dwrv_doorjam00.nif -Decoding meshes\i\in_dwrv_oilslick00.nif -Decoding meshes\i\in_strong_corr2_03.nif -Decoding meshes\i\in_strong_corr2_02.nif -Decoding meshes\i\in_strong_corr2_01.nif -Decoding meshes\i\in_strong_corr2_00.nif -Decoding meshes\i\in_strong_corr2_04.nif -Decoding meshes\i\in_strong_corr4_04.nif -Decoding meshes\i\in_strong_corr4_03.nif -Decoding meshes\i\in_strong_corr4_02.nif -Decoding meshes\i\in_strong_corr4_01.nif -Decoding meshes\i\in_strong_corr4_00.nif -Decoding meshes\i\in_strong_corr1_00.nif -Decoding meshes\i\in_strong_rubble01.nif -Decoding meshes\i\in_strong_rubble00.nif -Decoding meshes\i\in_strong_corr3_02.nif -Decoding meshes\i\in_strong_corr3_01.nif -Decoding meshes\i\in_strong_corr3_00.nif -Decoding meshes\i\in_nord_ladder_02.nif -Decoding meshes\i\in_nord_ladder_01.nif -Decoding meshes\i\in_t_railing_pole.nif -Decoding meshes\i\in_t_root_wrap_02.nif -Decoding meshes\i\in_t_root_wrap_01.nif -Decoding meshes\i\in_t_t_room_center.nif -Decoding meshes\i\in_sewer_bcorr4_00.nif -Decoding meshes\i\in_sewer_scorr4_00.nif -Decoding meshes\i\in_sewer_bcorr2_03.nif -Decoding meshes\i\in_sewer_bcorr2_02.nif -Decoding meshes\i\in_sewer_bcorr2_01.nif -Decoding meshes\i\in_sewer_bcorr2_00.nif -Decoding meshes\i\in_sewer_bcorr2_05.nif -Decoding meshes\i\in_sewer_bcorr2_04.nif -Decoding meshes\i\in_sewer_scorr2_02.nif -Decoding meshes\i\in_sewer_scorr2_01.nif -Decoding meshes\i\in_sewer_scorr2_00.nif -Decoding meshes\i\in_sewer_anchor00.nif -Decoding meshes\i\in_sewer_pillar00.nif -Decoding meshes\i\in_sewer_bcorr3_00.nif -Decoding meshes\i\in_sewer_scorr1_00.nif -Decoding meshes\i\in_sewer_bcorr1_01.nif -Decoding meshes\i\in_sewer_bcorr1_00.nif -Decoding meshes\i\in_vivec_ent_s_01.nif -Decoding meshes\i\in_cavern_stairs00.nif -Decoding meshes\i\in_cavern_roots10.nif -Decoding meshes\i\in_cavern_roots00.nif -Decoding meshes\i\in_lavacave2_s_00.nif -Decoding meshes\i\in_lavacave2_s_01.nif -Decoding meshes\i\in_lavacave2_s_02.nif -Decoding meshes\i\in_lavacave2_s_03.nif -Decoding meshes\i\in_lavacave2_s_04.nif -Decoding meshes\i\in_lavacave2_s_05.nif -Decoding meshes\i\in_lavacave2_s_06.nif -Decoding meshes\i\in_lavacave_form20.nif -Decoding meshes\i\in_lavacave_form30.nif -Decoding meshes\i\in_lavacave_form00.nif -Decoding meshes\i\in_lavacave_form10.nif -Decoding meshes\i\in_lavacave_exit00.nif -Decoding meshes\i\in_lavacave2_s_end.nif -Decoding meshes\i\in_lavacave4_s_00.nif -Decoding meshes\i\in_lavacave_stal00.nif -Decoding meshes\i\in_lavacave_stal10.nif -Decoding meshes\i\in_lavacave_stal20.nif -Decoding meshes\i\in_lavacave_stal30.nif -Decoding meshes\i\in_lavacave_stal40.nif -Decoding meshes\i\in_lavacave_stal50.nif -Decoding meshes\i\in_pycave2_s_end.nif -Decoding meshes\i\in_pycave_stal10.nif -Decoding meshes\i\in_pycave_stal00.nif -Decoding meshes\i\in_pycave2_s_05.nif -Decoding meshes\i\in_pycave2_s_03.nif -Decoding meshes\i\in_pycave2_s_01.nif -Decoding meshes\i\in_pycave_stal30.nif -Decoding meshes\i\in_pycave_stal20.nif -Decoding meshes\i\in_pycave_stal50.nif -Decoding meshes\i\in_pycave_stal40.nif -Decoding meshes\i\in_pycave_exit00.nif -Decoding meshes\i\in_pycave4_s_00.nif -Decoding meshes\i\in_pycave2_s_06.nif -Decoding meshes\i\in_pycave2_s_04.nif -Decoding meshes\i\in_pycave2_s_02.nif -Decoding meshes\i\in_pycave2_s_00.nif -Decoding meshes\i\in_pycave_form00.nif -Decoding meshes\i\in_pycave_form10.nif -Decoding meshes\i\in_pycave_form20.nif -Decoding meshes\i\in_pycave_form30.nif -Decoding meshes\i\in_dwrv_scope50.nif -Decoding meshes\i\in_dwrv_shaft10.nif -Decoding meshes\i\in_dwrv_shaft00.nif -Decoding meshes\i\in_dwrv_crank00.nif -Decoding meshes\i\in_dwrv_scope20.nif -Decoding meshes\i\in_dwrv_obsrv10.nif -Decoding meshes\i\in_dwrv_corr4_01.nif -Decoding meshes\i\in_dwrv_corr4_00.nif -Decoding meshes\i\in_dwrv_corr4_03.nif -Decoding meshes\i\in_dwrv_corr4_02.nif -Decoding meshes\i\in_dwrv_corr4_05.nif -Decoding meshes\i\in_dwrv_corr4_04.nif -Decoding meshes\i\in_dwrv_corr2_01.nif -Decoding meshes\i\in_dwrv_corr2_00.nif -Decoding meshes\i\in_dwrv_corr2_03.nif -Decoding meshes\i\in_dwrv_corr2_02.nif -Decoding meshes\i\in_dwrv_corr2_05.nif -Decoding meshes\i\in_dwrv_corr2_04.nif -Decoding meshes\i\in_dwrv_corr2_07.nif -Decoding meshes\i\in_dwrv_corr2_06.nif -Decoding meshes\i\in_dwrv_corr3_01.nif -Decoding meshes\i\in_dwrv_corr3_00.nif -Decoding meshes\i\in_dwrv_corr3_02.nif -Decoding meshes\i\in_dwrv_corr1_00.nif -Decoding meshes\i\in_dwrv_corr2_30.nif -Decoding meshes\i\in_dwrv_scope40.nif -Decoding meshes\i\in_dwrv_hall4_03.nif -Decoding meshes\i\in_dwrv_hall4_02.nif -Decoding meshes\i\in_dwrv_hall4_01.nif -Decoding meshes\i\in_dwrv_hall4_00.nif -Decoding meshes\i\in_dwrv_hall2_00.nif -Decoding meshes\i\in_dwrv_hall3_02.nif -Decoding meshes\i\in_dwrv_hall3_00.nif -Decoding meshes\i\in_dwrv_scope30.nif -Decoding meshes\i\in_dwrv_scope00.nif -Decoding meshes\i\in_dwrv_obsrv00.nif -Decoding meshes\i\in_dwrv_scope10.nif -Decoding meshes\i\in_mudcave2_s_00.nif -Decoding meshes\i\in_mudcave2_s_01.nif -Decoding meshes\i\in_mudcave2_s_02.nif -Decoding meshes\i\in_mudcave2_s_03.nif -Decoding meshes\i\in_mudcave2_s_04.nif -Decoding meshes\i\in_mudcave2_s_05.nif -Decoding meshes\i\in_mudcave2_s_06.nif -Decoding meshes\i\in_mudcave4_s_00.nif -Decoding meshes\i\in_mudboulder04.nif -Decoding meshes\i\in_mudboulder00.nif -Decoding meshes\i\in_mudboulder02.nif -Decoding meshes\i\in_mudboulder05.nif -Decoding meshes\i\in_mudboulder01.nif -Decoding meshes\i\in_mudboulder03.nif -Decoding meshes\i\in_mudcave2_end.nif -Decoding meshes\i\in_mudcave_28_1.nif -Decoding meshes\i\in_mudcave_21_1.nif -Decoding meshes\i\in_strong_ramp10.nif -Decoding meshes\i\in_strong_ramp00.nif -Decoding meshes\i\in_strong_hall02.nif -Decoding meshes\i\in_strong_hall03.nif -Decoding meshes\i\in_strong_hall00.nif -Decoding meshes\i\in_strong_hall01.nif -Decoding meshes\i\in_strong_hall06.nif -Decoding meshes\i\in_strong_hall04.nif -Decoding meshes\i\in_strong_hall05.nif -Decoding meshes\i\in_vs_pitct_b_01.nif -Decoding meshes\i\in_vs_pitd_b_02.nif -Decoding meshes\i\in_vs_pittw_b_01.nif -Decoding meshes\i\in_vs_pitmw_b_01.nif -Decoding meshes\i\in_vs_pitb_b_01.nif -Decoding meshes\i\in_vs_pitbw_b_01.nif -Decoding meshes\i\in_vs_pitceil_01.nif -Decoding meshes\i\in_vs_pitcb_b_01.nif -Decoding meshes\i\in_vs_pitd_b_01.nif -Decoding meshes\i\in_vs_pitd_b_03.nif -Decoding meshes\i\in_vs_pittd_b_01.nif -Decoding meshes\i\in_vs_pitcm_b_01.nif -Decoding meshes\i\in_vsl_pitc_b_01.nif -Decoding meshes\i\in_ashl_door_01.nif -Decoding meshes\i\in_ashl_tent_05.nif -Decoding meshes\i\in_ashl_tent_03.nif -Decoding meshes\i\in_ashl_tent_01.nif -Decoding meshes\i\in_ashl_door_02.nif -Decoding meshes\i\in_ashl_tent_04.nif -Decoding meshes\i\in_ashl_tent_02.nif -Decoding meshes\i\in_ar_bridge_10.nif -Decoding meshes\i\in_ar_bridge_09.nif -Decoding meshes\i\in_ar_bridge_01.nif -Decoding meshes\i\in_ar_bridge_03.nif -Decoding meshes\i\in_ar_bridge_05.nif -Decoding meshes\i\in_ar_bridge_07.nif -Decoding meshes\i\in_ar_bridge_08.nif -Decoding meshes\i\in_ar_bridge_02.nif -Decoding meshes\i\in_ar_bridge_04.nif -Decoding meshes\i\in_ar_bridge_06.nif -Decoding meshes\i\in_bone_rock_25.nif -Decoding meshes\i\in_bone_rock_27.nif -Decoding meshes\i\in_bone_rock_21.nif -Decoding meshes\i\in_bone_rock_23.nif -Decoding meshes\i\in_bonecave2_11.nif -Decoding meshes\i\in_bonecave2_end.nif -Decoding meshes\i\in_bone_rock_18.nif -Decoding meshes\i\in_bone_rock_12.nif -Decoding meshes\i\in_bone_rock_10.nif -Decoding meshes\i\in_bone_rock_16.nif -Decoding meshes\i\in_bone_rock_14.nif -Decoding meshes\i\in_bonecave4_00.nif -Decoding meshes\i\in_bonecave2_02.nif -Decoding meshes\i\in_bonecave2_00.nif -Decoding meshes\i\in_bonecave2_06.nif -Decoding meshes\i\in_bonecave2_04.nif -Decoding meshes\i\in_bonecave2_08.nif -Decoding meshes\i\in_bone_rock_05.nif -Decoding meshes\i\in_bone_rock_07.nif -Decoding meshes\i\in_bone_rock_01.nif -Decoding meshes\i\in_bone_rock_03.nif -Decoding meshes\i\in_bone_rock_09.nif -Decoding meshes\i\in_boneboulder01.nif -Decoding meshes\i\in_boneboulder00.nif -Decoding meshes\i\in_boneboulder03.nif -Decoding meshes\i\in_boneboulder02.nif -Decoding meshes\i\in_boneboulder05.nif -Decoding meshes\i\in_boneboulder04.nif -Decoding meshes\i\in_bonetrans_00.nif -Decoding meshes\i\in_bone_rock_24.nif -Decoding meshes\i\in_bone_rock_26.nif -Decoding meshes\i\in_bone_rock_20.nif -Decoding meshes\i\in_bone_rock_22.nif -Decoding meshes\i\in_bone_rock_28.nif -Decoding meshes\i\in_bonecave_28_1.nif -Decoding meshes\i\in_bonecave2_10.nif -Decoding meshes\i\in_bonecave2_12.nif -Decoding meshes\i\in_bonecave_21_1.nif -Decoding meshes\i\in_bone_rock_19.nif -Decoding meshes\i\in_bone_rock_13.nif -Decoding meshes\i\in_bone_rock_11.nif -Decoding meshes\i\in_bone_rock_17.nif -Decoding meshes\i\in_bone_rock_15.nif -Decoding meshes\i\in_bonecave2_03.nif -Decoding meshes\i\in_bonecave2_01.nif -Decoding meshes\i\in_bonecave2_07.nif -Decoding meshes\i\in_bonecave2_05.nif -Decoding meshes\i\in_bonecave2_09.nif -Decoding meshes\i\in_bone_rock_04.nif -Decoding meshes\i\in_bone_rock_06.nif -Decoding meshes\i\in_bone_rock_02.nif -Decoding meshes\i\in_bone_rock_08.nif -Decoding meshes\i\in_moldcave_21_1.nif -Decoding meshes\i\in_mold_rock_25.nif -Decoding meshes\i\in_mold_rock_27.nif -Decoding meshes\i\in_mold_rock_21.nif -Decoding meshes\i\in_mold_rock_23.nif -Decoding meshes\i\in_mold_rock_19.nif -Decoding meshes\i\in_mold_rock_13.nif -Decoding meshes\i\in_mold_rock_11.nif -Decoding meshes\i\in_mold_rock_17.nif -Decoding meshes\i\in_mold_rock_15.nif -Decoding meshes\i\in_moldcave2_02.nif -Decoding meshes\i\in_moldcave2_00.nif -Decoding meshes\i\in_moldcave2_06.nif -Decoding meshes\i\in_moldcave2_04.nif -Decoding meshes\i\in_moldcave2_08.nif -Decoding meshes\i\in_moldcave4_00.nif -Decoding meshes\i\in_mold_rock_07.nif -Decoding meshes\i\in_mold_rock_05.nif -Decoding meshes\i\in_mold_rock_03.nif -Decoding meshes\i\in_mold_rock_01.nif -Decoding meshes\i\in_mold_rock_09.nif -Decoding meshes\i\in_moldtrans_00.nif -Decoding meshes\i\in_moldboulder05.nif -Decoding meshes\i\in_moldboulder04.nif -Decoding meshes\i\in_moldboulder03.nif -Decoding meshes\i\in_moldboulder02.nif -Decoding meshes\i\in_moldboulder01.nif -Decoding meshes\i\in_moldboulder00.nif -Decoding meshes\i\in_mold_rock_24.nif -Decoding meshes\i\in_mold_rock_26.nif -Decoding meshes\i\in_mold_rock_20.nif -Decoding meshes\i\in_mold_rock_22.nif -Decoding meshes\i\in_mold_rock_28.nif -Decoding meshes\i\in_moldcave2_end.nif -Decoding meshes\i\in_mold_rock_18.nif -Decoding meshes\i\in_mold_rock_12.nif -Decoding meshes\i\in_mold_rock_10.nif -Decoding meshes\i\in_mold_rock_16.nif -Decoding meshes\i\in_mold_rock_14.nif -Decoding meshes\i\in_moldcave2_03.nif -Decoding meshes\i\in_moldcave2_01.nif -Decoding meshes\i\in_moldcave2_07.nif -Decoding meshes\i\in_moldcave2_05.nif -Decoding meshes\i\in_moldcave2_09.nif -Decoding meshes\i\in_moldcave_28_1.nif -Decoding meshes\i\in_mold_rock_06.nif -Decoding meshes\i\in_mold_rock_04.nif -Decoding meshes\i\in_mold_rock_02.nif -Decoding meshes\i\in_mold_rock_08.nif -Decoding meshes\i\in_nord_house_03.nif -Decoding meshes\i\in_nord_house_02.nif -Decoding meshes\i\in_nord_house_01.nif -Decoding meshes\i\in_nord_house_05.nif -Decoding meshes\i\in_nord_house_04.nif -Decoding meshes\i\in_nord_doorf_01.nif -Decoding meshes\i\in_nord_doorf_02.nif -Decoding meshes\i\in_shipwreck_top.nif -Decoding meshes\i\in_sewer_canal00.nif -Decoding meshes\i\in_sewer_raft00.nif -Decoding meshes\i\in_sewer_union00.nif -Decoding meshes\i\in_de_shipcabin.nif -Decoding meshes\i\in_de_shack_door.nif -Decoding meshes\i\in_dae_hall_l_03.nif -Decoding meshes\i\in_dae_hall_l_02.nif -Decoding meshes\i\in_dae_hall_l_01.nif -Decoding meshes\i\in_cavern_ramp10.nif -Decoding meshes\i\in_cavern_ramp00.nif -Decoding meshes\i\in_cave_plant00.nif -Decoding meshes\i\in_cavern_beam40.nif -Decoding meshes\i\in_cavern_beam00.nif -Decoding meshes\i\in_cavern_beam10.nif -Decoding meshes\i\in_cavern_beam20.nif -Decoding meshes\i\in_cavern_beam30.nif -Decoding meshes\i\in_cave_plant10.nif -Decoding meshes\i\in_cavern_rail00.nif -Decoding meshes\i\in_cavern_rail10.nif -Decoding meshes\i\in_lava_rock_23.nif -Decoding meshes\i\in_lava_rock_21.nif -Decoding meshes\i\in_lava_rock_27.nif -Decoding meshes\i\in_lava_rock_25.nif -Decoding meshes\i\in_lavacave_21_1.nif -Decoding meshes\i\in_lavatrans_00.nif -Decoding meshes\i\in_lavacave2_end.nif -Decoding meshes\i\in_lava_rock_16.nif -Decoding meshes\i\in_lava_rock_14.nif -Decoding meshes\i\in_lava_rock_12.nif -Decoding meshes\i\in_lava_rock_10.nif -Decoding meshes\i\in_lava_rock_18.nif -Decoding meshes\i\in_lavacave2_05.nif -Decoding meshes\i\in_lavacave2_07.nif -Decoding meshes\i\in_lavacave2_01.nif -Decoding meshes\i\in_lavacave2_03.nif -Decoding meshes\i\in_lavacave2_09.nif -Decoding meshes\i\in_lava_rock_09.nif -Decoding meshes\i\in_lava_rock_05.nif -Decoding meshes\i\in_lava_rock_07.nif -Decoding meshes\i\in_lava_rock_01.nif -Decoding meshes\i\in_lava_rock_03.nif -Decoding meshes\i\in_lava_rock_28.nif -Decoding meshes\i\in_lava_rock_22.nif -Decoding meshes\i\in_lava_rock_20.nif -Decoding meshes\i\in_lava_rock_26.nif -Decoding meshes\i\in_lava_rock_24.nif -Decoding meshes\i\in_lavacave_28_1.nif -Decoding meshes\i\in_lava_1024_01.nif -Decoding meshes\i\in_lavaboulder02.nif -Decoding meshes\i\in_lavaboulder03.nif -Decoding meshes\i\in_lavaboulder00.nif -Decoding meshes\i\in_lavaboulder01.nif -Decoding meshes\i\in_lavaboulder04.nif -Decoding meshes\i\in_lavaboulder05.nif -Decoding meshes\i\in_lava_rock_17.nif -Decoding meshes\i\in_lava_rock_15.nif -Decoding meshes\i\in_lava_rock_13.nif -Decoding meshes\i\in_lava_rock_11.nif -Decoding meshes\i\in_lava_rock_19.nif -Decoding meshes\i\in_lavacave4_00.nif -Decoding meshes\i\in_lavacave2_04.nif -Decoding meshes\i\in_lavacave2_06.nif -Decoding meshes\i\in_lavacave2_00.nif -Decoding meshes\i\in_lavacave2_02.nif -Decoding meshes\i\in_lavacave2_08.nif -Decoding meshes\i\in_lava_rock_08.nif -Decoding meshes\i\in_lava_rock_04.nif -Decoding meshes\i\in_lava_rock_06.nif -Decoding meshes\i\in_lava_rock_02.nif -Decoding meshes\i\in_t_crystal_02.nif -Decoding meshes\i\in_t_s_shaft_01.nif -Decoding meshes\i\in_t_l_hall_3way.nif -Decoding meshes\i\in_t_housepod_01.nif -Decoding meshes\i\in_t_housepod_02.nif -Decoding meshes\i\in_t_s_pillar_04.nif -Decoding meshes\i\in_t_s_pillar_02.nif -Decoding meshes\i\in_t_s_hall_4way.nif -Decoding meshes\i\in_t_s_hall_3way.nif -Decoding meshes\i\in_t_councilhall.nif -Decoding meshes\i\in_t_gatesymbol.nif -Decoding meshes\i\in_t_l_pillar_01.nif -Decoding meshes\i\in_t_crystal_01.nif -Decoding meshes\i\in_t_l_room_side.nif -Decoding meshes\i\in_t_platform_01.nif -Decoding meshes\i\in_t_platform_02.nif -Decoding meshes\i\in_t_s_turret_03.nif -Decoding meshes\i\in_t_s_turret_02.nif -Decoding meshes\i\in_t_sidewall_01.nif -Decoding meshes\i\in_t_sidewall_02.nif -Decoding meshes\i\in_v_roomhol_01.nif -Decoding meshes\i\in_r_building_01.nif -Decoding meshes\i\in_c_stone_4way.nif -Decoding meshes\i\in_c_stone_3way.nif -Decoding meshes\i\in_c_railing_01.nif -Decoding meshes\i\in_c_wall_plain.nif -Decoding meshes\i\in_c_plain_4way.nif -Decoding meshes\i\in_c_plain_3way.nif -Decoding meshes\i\in_c_pillar_wood.nif -Decoding meshes\i\in_t_l_hall_01.nif -Decoding meshes\i\in_t_s_hall_01.nif -Decoding meshes\i\in_t_gasket_01.nif -Decoding meshes\i\in_t_manor_01.nif -Decoding meshes\i\in_t_manor_02.nif -Decoding meshes\i\in_t_l_door_01.nif -Decoding meshes\i\in_v_palace_01.nif -Decoding meshes\i\in_v_plaza_01.nif -Decoding meshes\i\in_v_roomhf_01.nif -Decoding meshes\i\in_v_arena_01.nif -Decoding meshes\i\in_v_plaza_02.nif -Decoding meshes\i\in_pytrans_00.nif -Decoding meshes\i\in_py_rock_15.nif -Decoding meshes\i\in_pycave_28_1.nif -Decoding meshes\i\in_pycave_21_1.nif -Decoding meshes\i\in_py_rock_19.nif -Decoding meshes\i\in_py_rock_09.nif -Decoding meshes\i\in_pycave2_06.nif -Decoding meshes\i\in_py_rock_02.nif -Decoding meshes\i\in_py_rock_12.nif -Decoding meshes\i\in_py_rock_22.nif -Decoding meshes\i\in_pycave2_00.nif -Decoding meshes\i\in_pycave4_00.nif -Decoding meshes\i\in_py_rock_05.nif -Decoding meshes\i\in_py_rock_23.nif -Decoding meshes\i\in_py_rock_03.nif -Decoding meshes\i\in_pycave2_01.nif -Decoding meshes\i\in_py_rock_06.nif -Decoding meshes\i\in_pycave2_07.nif -Decoding meshes\i\in_py_rock_16.nif -Decoding meshes\i\in_py_rock_20.nif -Decoding meshes\i\in_py_rock_10.nif -Decoding meshes\i\in_pycave2_04.nif -Decoding meshes\i\in_py_rock_26.nif -Decoding meshes\i\in_py_rock_27.nif -Decoding meshes\i\in_pyboulder04.nif -Decoding meshes\i\in_pyboulder00.nif -Decoding meshes\i\in_pyboulder02.nif -Decoding meshes\i\in_py_rock_25.nif -Decoding meshes\i\in_pycave2_03.nif -Decoding meshes\i\in_py_rock_07.nif -Decoding meshes\i\in_pycave2_02.nif -Decoding meshes\i\in_py_rock_24.nif -Decoding meshes\i\in_py_rock_17.nif -Decoding meshes\i\in_prison_ship.nif -Decoding meshes\i\in_pycave2_09.nif -Decoding meshes\i\in_py_rock_14.nif -Decoding meshes\i\in_py_rock_13.nif -Decoding meshes\i\in_pycave2_05.nif -Decoding meshes\i\in_pyboulder05.nif -Decoding meshes\i\in_pyboulder01.nif -Decoding meshes\i\in_pyboulder03.nif -Decoding meshes\i\in_py_rock_28.nif -Decoding meshes\i\in_py_rock_18.nif -Decoding meshes\i\in_pycave2_end.nif -Decoding meshes\i\in_py_rock_08.nif -Decoding meshes\i\in_py_rock_04.nif -Decoding meshes\i\in_pycave2_08.nif -Decoding meshes\i\in_py_rock_11.nif -Decoding meshes\i\in_py_rock_01.nif -Decoding meshes\i\in_py_rock_21.nif -Decoding meshes\i\in_r_s_room_02.nif -Decoding meshes\i\in_r_s_room_01.nif -Decoding meshes\i\in_r_s_room_03.nif -Decoding meshes\i\in_dwrv_gear10.nif -Decoding meshes\i\in_de_shack_05.nif -Decoding meshes\i\in_de_shack_03.nif -Decoding meshes\i\in_de_shack_01.nif -Decoding meshes\i\in_dwrv_gear00.nif -Decoding meshes\i\in_dwrv_wall10.nif -Decoding meshes\i\in_dwrv_lift00.nif -Decoding meshes\i\in_de_shack_04.nif -Decoding meshes\i\in_de_shack_02.nif -Decoding meshes\i\in_dwrv_wall00.nif -Decoding meshes\i\in_dwrv_gear20.nif -Decoding meshes\i\in_dae_claw_01.nif -Decoding meshes\i\in_dwrv_gear30.nif -Decoding meshes\i\in_akulacave00.nif -Decoding meshes\i\in_akulakhan00.nif -Decoding meshes\i\in_bonecave_17.nif -Decoding meshes\i\in_bonecave_15.nif -Decoding meshes\i\in_bonecave_13.nif -Decoding meshes\i\in_bonecave_11.nif -Decoding meshes\i\in_bonecave_19.nif -Decoding meshes\i\in_bonecave_21.nif -Decoding meshes\i\in_bonecave_27.nif -Decoding meshes\i\in_bonecave_25.nif -Decoding meshes\i\in_bonecave_08.nif -Decoding meshes\i\in_bonecave_06.nif -Decoding meshes\i\in_bonecave_04.nif -Decoding meshes\i\in_bonecave_02.nif -Decoding meshes\i\in_bonecave_00.nif -Decoding meshes\i\in_bonecave_16.nif -Decoding meshes\i\in_bonecave_14.nif -Decoding meshes\i\in_bonecave_12.nif -Decoding meshes\i\in_bonecave_10.nif -Decoding meshes\i\in_bonecave_18.nif -Decoding meshes\i\in_bonecave_26.nif -Decoding meshes\i\in_bonecave_09.nif -Decoding meshes\i\in_bonecave_07.nif -Decoding meshes\i\in_bonecave_05.nif -Decoding meshes\i\in_bonecave_03.nif -Decoding meshes\i\in_bonecave_01.nif -Decoding meshes\i\in_ci_mummy_04.nif -Decoding meshes\i\in_ci_mummy_06.nif -Decoding meshes\i\in_ci_mummy_02.nif -Decoding meshes\i\in_ci_azura_01.nif -Decoding meshes\i\in_c_wall_rich.nif -Decoding meshes\i\in_ci_mummy_05.nif -Decoding meshes\i\in_ci_mummy_01.nif -Decoding meshes\i\in_ci_mummy_03.nif -Decoding meshes\i\in_lavacave_17.nif -Decoding meshes\i\in_lavacave_15.nif -Decoding meshes\i\in_lavacave_13.nif -Decoding meshes\i\in_lavacave_11.nif -Decoding meshes\i\in_lavacave_19.nif -Decoding meshes\i\in_lavalayer00.nif -Decoding meshes\i\in_lavacave_08.nif -Decoding meshes\i\in_lavacave_06.nif -Decoding meshes\i\in_lavacave_04.nif -Decoding meshes\i\in_lavacave_02.nif -Decoding meshes\i\in_lavacave_00.nif -Decoding meshes\i\in_lavacave_21.nif -Decoding meshes\i\in_lavacave_27.nif -Decoding meshes\i\in_lavacave_25.nif -Decoding meshes\i\in_lavacave_16.nif -Decoding meshes\i\in_lavacave_14.nif -Decoding meshes\i\in_lavacave_12.nif -Decoding meshes\i\in_lavacave_10.nif -Decoding meshes\i\in_lavacave_18.nif -Decoding meshes\i\in_lavacave_09.nif -Decoding meshes\i\in_lavacave_07.nif -Decoding meshes\i\in_lavacave_05.nif -Decoding meshes\i\in_lavacave_03.nif -Decoding meshes\i\in_lavacave_01.nif -Decoding meshes\i\in_lavacave_26.nif -Decoding meshes\i\in_mudcave_14.nif -Decoding meshes\i\in_mudcave_07.nif -Decoding meshes\i\in_mudcave_17.nif -Decoding meshes\i\in_mudtrans_00.nif -Decoding meshes\i\in_mudcave_27.nif -Decoding meshes\i\in_moldcave_17.nif -Decoding meshes\i\in_moldcave_15.nif -Decoding meshes\i\in_moldcave_13.nif -Decoding meshes\i\in_moldcave_11.nif -Decoding meshes\i\in_moldcave_19.nif -Decoding meshes\i\in_mudcave_10.nif -Decoding meshes\i\in_mudcave_00.nif -Decoding meshes\i\in_mudcave_03.nif -Decoding meshes\i\in_mudcave2_08.nif -Decoding meshes\i\in_mudcave2_04.nif -Decoding meshes\i\in_mudcave2_06.nif -Decoding meshes\i\in_mudcave2_00.nif -Decoding meshes\i\in_mudcave2_02.nif -Decoding meshes\i\in_mudcave4_00.nif -Decoding meshes\i\in_mud_rock_21.nif -Decoding meshes\i\in_mud_rock_23.nif -Decoding meshes\i\in_mud_rock_25.nif -Decoding meshes\i\in_mud_rock_27.nif -Decoding meshes\i\in_mudcave_04.nif -Decoding meshes\i\in_mudcave_21.nif -Decoding meshes\i\in_mudcave_11.nif -Decoding meshes\i\in_mudcave_01.nif -Decoding meshes\i\in_mudcave_18.nif -Decoding meshes\i\in_mudcave_08.nif -Decoding meshes\i\in_mud_rock_13.nif -Decoding meshes\i\in_mud_rock_11.nif -Decoding meshes\i\in_mud_rock_17.nif -Decoding meshes\i\in_mud_rock_15.nif -Decoding meshes\i\in_mud_rock_19.nif -Decoding meshes\i\in_moldcave_21.nif -Decoding meshes\i\in_moldcave_27.nif -Decoding meshes\i\in_moldcave_25.nif -Decoding meshes\i\in_moldcave_08.nif -Decoding meshes\i\in_moldcave_06.nif -Decoding meshes\i\in_moldcave_04.nif -Decoding meshes\i\in_moldcave_02.nif -Decoding meshes\i\in_moldcave_00.nif -Decoding meshes\i\in_mud_rock_07.nif -Decoding meshes\i\in_mud_rock_05.nif -Decoding meshes\i\in_mud_rock_03.nif -Decoding meshes\i\in_mud_rock_01.nif -Decoding meshes\i\in_mud_rock_09.nif -Decoding meshes\i\in_mudcave_26.nif -Decoding meshes\i\in_moldcave_16.nif -Decoding meshes\i\in_moldcave_14.nif -Decoding meshes\i\in_moldcave_12.nif -Decoding meshes\i\in_moldcave_10.nif -Decoding meshes\i\in_moldcave_18.nif -Decoding meshes\i\in_mudcave_13.nif -Decoding meshes\i\in_mudcave_06.nif -Decoding meshes\i\in_mudcave_25.nif -Decoding meshes\i\in_mudcave2_09.nif -Decoding meshes\i\in_mudcave2_05.nif -Decoding meshes\i\in_mudcave2_07.nif -Decoding meshes\i\in_mudcave2_01.nif -Decoding meshes\i\in_mudcave2_03.nif -Decoding meshes\i\in_mudcave_16.nif -Decoding meshes\i\in_mud_rock_20.nif -Decoding meshes\i\in_mud_rock_22.nif -Decoding meshes\i\in_mud_rock_24.nif -Decoding meshes\i\in_mud_rock_26.nif -Decoding meshes\i\in_mud_rock_28.nif -Decoding meshes\i\in_mudcave_15.nif -Decoding meshes\i\in_mud_rock_12.nif -Decoding meshes\i\in_mud_rock_10.nif -Decoding meshes\i\in_mud_rock_16.nif -Decoding meshes\i\in_mud_rock_14.nif -Decoding meshes\i\in_mud_rock_18.nif -Decoding meshes\i\in_moldcave_26.nif -Decoding meshes\i\in_moldcave_09.nif -Decoding meshes\i\in_moldcave_07.nif -Decoding meshes\i\in_moldcave_05.nif -Decoding meshes\i\in_moldcave_03.nif -Decoding meshes\i\in_moldcave_01.nif -Decoding meshes\i\in_mud_rock_06.nif -Decoding meshes\i\in_mud_rock_04.nif -Decoding meshes\i\in_mud_rock_02.nif -Decoding meshes\i\in_mud_rock_08.nif -Decoding meshes\i\in_mudcave_05.nif -Decoding meshes\i\in_mudcave_02.nif -Decoding meshes\i\in_mudcave_12.nif -Decoding meshes\i\in_mudcave_19.nif -Decoding meshes\i\in_mudcave_09.nif -Decoding meshes\i\in_hlaalu_door.nif -Decoding meshes\i\in_hlaalu_wall.nif -Decoding meshes\i\in_icesheet_01.nif -Decoding meshes\i\in_icesheet_02.nif -Decoding meshes\i\in_pycave_15.nif -Decoding meshes\i\in_pycave_02.nif -Decoding meshes\i\in_pycave_12.nif -Decoding meshes\i\in_lava_256a.nif -Decoding meshes\i\in_pycave_05.nif -Decoding meshes\i\in_lava_oval.nif -Decoding meshes\i\in_pycave_06.nif -Decoding meshes\i\in_pycave_03.nif -Decoding meshes\i\in_pycave_16.nif -Decoding meshes\i\in_pycave_19.nif -Decoding meshes\i\in_pycave_26.nif -Decoding meshes\i\in_pycave_09.nif -Decoding meshes\i\in_pycave_27.nif -Decoding meshes\i\in_pycave_18.nif -Decoding meshes\i\in_pycave_25.nif -Decoding meshes\i\in_pycave_08.nif -Decoding meshes\i\in_pycave_10.nif -Decoding meshes\i\in_pycave_00.nif -Decoding meshes\i\in_pycave_07.nif -Decoding meshes\i\in_pycave_17.nif -Decoding meshes\i\in_lava_256.nif -Decoding meshes\i\in_t_l_4way.nif -Decoding meshes\i\in_t_edge_01.nif -Decoding meshes\i\in_pycave_14.nif -Decoding meshes\i\in_lava_512.nif -Decoding meshes\i\in_pycave_13.nif -Decoding meshes\i\in_lava_1024.nif -Decoding meshes\i\in_pycave_11.nif -Decoding meshes\i\in_pycave_01.nif -Decoding meshes\i\in_pycave_21.nif -Decoding meshes\i\in_pycave_04.nif -Decoding meshes\i\in_c_skywalk.nif -Decoding meshes\i\in_6th_chalk30.nif -Decoding meshes\i\in_6th_chalk20.nif -Decoding meshes\i\in_6th_chalk00.nif -Decoding meshes\i\in_6th_chalk10.nif -Decoding meshes\i\tx_crystal_03.nif -Decoding meshes\i\tx_crystal_02.nif -Decoding meshes\i\ex_dae_ruin_04.nif -Decoding meshes\i\ex_dae_ruin_02.nif -Decoding meshes\a\a_imperial_f_shoe.nif -Decoding meshes\a\a_imperial_a_boot.nif -Decoding meshes\a\a_iron_cuirass_gnd.nif -Decoding meshes\a\a_iron_pauldron_fa.nif -Decoding meshes\a\a_iron_pauldron_ua.nif -Decoding meshes\a\a_iron_pauldron_cl.nif -Decoding meshes\a\a_iron_greaves_gnd.nif -Decoding meshes\a\a_iron_greaves_ul.nif -Decoding meshes\a\a_indoril_m_helmet.nif -Decoding meshes\a\a_indoril_m_f_boot.nif -Decoding meshes\a\a_indoril_m_a_boot.nif -Decoding meshes\a\a_indoril_m_skins.nif -Decoding meshes\a\shield_art_auriel.nif -Decoding meshes\marker_creature.nif -Decoding meshes\marker_travel.nif -Decoding meshes\marker_temple.nif -Decoding meshes\marker_prison.nif -Decoding meshes\marker_radius.nif -Decoding meshes\marker_divine.nif -Decoding meshes\m\skeleton_key.nif -Decoding meshes\a\shield_bonemold.nif -Decoding meshes\a\shield_dwemer.nif -Decoding meshes\a\shield_daedric.nif -Decoding meshes\a\shield_dreugh.nif -Decoding meshes\a\shield_indoril.nif -Decoding meshes\a\shield_chitin.nif -Decoding meshes\a\shield_ebony.nif -Decoding meshes\a\shield_steel.nif -Decoding meshes\a\shield_glass.nif -Decoding meshes\a\shield_iron.nif -Decoding meshes\a\a_imperial_skins.nif -Decoding meshes\a\a_imperial_skirt.nif -Decoding meshes\a\a_iron_boot_gnd.nif -Decoding meshes\a\a_iron_greaves_g.nif -Decoding meshes\a\a_iron_greaves_k.nif -Decoding meshes\a\a_iron_bracer_w.nif -Decoding meshes\a\a_iron_hands.1st.nif -Decoding meshes\a\a_iron_boot_f.nif -Decoding meshes\a\a_iron_skinned.nif -Decoding meshes\a\a_iron_boot_a.nif -Decoding meshes\a\a_iron_helm_01.nif -Decoding meshes\f\flora_bc_grass_02.nif -Decoding meshes\f\flora_bc_grass_01.nif -Decoding meshes\f\flora_comberry_01.nif -Decoding meshes\f\flora_chokeweed_01.nif -Decoding meshes\f\flora_roobrush_01.nif -Decoding meshes\f\flora_corkbulb_01.nif -Decoding meshes\n\ingred_emerald_01.nif -Decoding meshes\n\ingred_comberry_01.nif -Decoding meshes\n\ingred_ash_yam_01.nif -Decoding meshes\n\ingred_russula_01.nif -Decoding meshes\n\ingred_coprinus_01.nif -Decoding meshes\n\ingred_scuttle_01.nif -Decoding meshes\n\ingred_saltrice_01.nif -Decoding meshes\n\ingred_roobrush_01.nif -Decoding meshes\n\ingred_heather_01.nif -Decoding meshes\n\ingred_rat_meat_01.nif -Decoding meshes\n\ingred_crabmeat_01.nif -Decoding meshes\n\ingred_rawebony_01.nif -Decoding meshes\n\ingred_diamond_01.nif -Decoding meshes\f\food_kwama_egg_01.nif -Decoding meshes\f\food_kwama_egg_02.nif -Decoding meshes\n\ingred_pearl_01.nif -Decoding meshes\n\ingred_bread_01.nif -Decoding meshes\n\ingred_scales_01.nif -Decoding meshes\n\ingred_resin_01.nif -Decoding meshes\n\ingred_muck_01.nif -Decoding meshes\n\ingred_ruby_01.nif -Decoding meshes\f\flora_root_wg_07.nif -Decoding meshes\f\flora_root_wg_06.nif -Decoding meshes\f\flora_root_wg_05.nif -Decoding meshes\f\flora_root_wg_04.nif -Decoding meshes\f\flora_root_wg_03.nif -Decoding meshes\f\flora_root_wg_02.nif -Decoding meshes\f\flora_root_wg_01.nif -Decoding meshes\f\flora_root_wg_08.nif -Decoding meshes\f\flora_bc_log_02.nif -Decoding meshes\f\flora_muckpod_04.nif -Decoding meshes\f\flora_muckpod_05.nif -Decoding meshes\f\flora_muckpod_02.nif -Decoding meshes\f\flora_muckpod_03.nif -Decoding meshes\f\flora_muckpod_01.nif -Decoding meshes\f\flora_bc_moss_03.nif -Decoding meshes\f\flora_bc_moss_02.nif -Decoding meshes\f\flora_bc_moss_01.nif -Decoding meshes\f\flora_bc_moss_07.nif -Decoding meshes\f\flora_bc_moss_06.nif -Decoding meshes\f\flora_bc_moss_05.nif -Decoding meshes\f\flora_bc_moss_04.nif -Decoding meshes\f\flora_bc_moss_09.nif -Decoding meshes\f\flora_bc_moss_08.nif -Decoding meshes\f\flora_bc_moss_13.nif -Decoding meshes\f\flora_bc_moss_12.nif -Decoding meshes\f\flora_bc_moss_11.nif -Decoding meshes\f\flora_bc_moss_10.nif -Decoding meshes\f\flora_bc_moss_17.nif -Decoding meshes\f\flora_bc_moss_16.nif -Decoding meshes\f\flora_bc_moss_15.nif -Decoding meshes\f\flora_bc_moss_14.nif -Decoding meshes\f\flora_bc_moss_19.nif -Decoding meshes\f\flora_bc_moss_18.nif -Decoding meshes\f\flora_bc_moss_21.nif -Decoding meshes\f\flora_bc_moss_20.nif -Decoding meshes\f\flora_bc_tree_13.nif -Decoding meshes\f\flora_bc_tree_12.nif -Decoding meshes\f\flora_bc_tree_11.nif -Decoding meshes\f\flora_bc_tree_10.nif -Decoding meshes\f\flora_bc_tree_03.nif -Decoding meshes\f\flora_bc_tree_02.nif -Decoding meshes\f\flora_bc_tree_01.nif -Decoding meshes\f\flora_bc_tree_07.nif -Decoding meshes\f\flora_bc_tree_06.nif -Decoding meshes\f\flora_bc_tree_05.nif -Decoding meshes\f\flora_bc_tree_04.nif -Decoding meshes\f\flora_bc_tree_09.nif -Decoding meshes\f\flora_bc_tree_08.nif -Decoding meshes\f\flora_bc_vine_02.nif -Decoding meshes\f\flora_bc_vine_03.nif -Decoding meshes\f\flora_bc_vine_01.nif -Decoding meshes\f\flora_bc_vine_06.nif -Decoding meshes\f\flora_bc_vine_07.nif -Decoding meshes\f\flora_bc_vine_04.nif -Decoding meshes\f\flora_bc_vine_05.nif -Decoding meshes\f\flora_bc_fern_04.nif -Decoding meshes\f\flora_bc_fern_03.nif -Decoding meshes\f\flora_bc_fern_02.nif -Decoding meshes\f\flora_bc_fern_01.nif -Decoding meshes\f\flora_bc_log_01.nif -Decoding meshes\f\flora_tree_gl_11.nif -Decoding meshes\f\flora_tree_gl_10.nif -Decoding meshes\f\flora_tree_ac_04.nif -Decoding meshes\f\flora_tree_ac_01.nif -Decoding meshes\f\flora_tree_ac_03.nif -Decoding meshes\f\flora_tree_ac_02.nif -Decoding meshes\f\flora_tree_wg_08.nif -Decoding meshes\f\flora_tree_wg_01.nif -Decoding meshes\f\flora_tree_wg_03.nif -Decoding meshes\f\flora_tree_wg_02.nif -Decoding meshes\f\flora_tree_wg_05.nif -Decoding meshes\f\flora_tree_wg_04.nif -Decoding meshes\f\flora_tree_wg_07.nif -Decoding meshes\f\flora_tree_wg_06.nif -Decoding meshes\f\flora_tree_ai_05.nif -Decoding meshes\f\flora_tree_ai_06.nif -Decoding meshes\f\flora_tree_gl_09.nif -Decoding meshes\f\flora_tree_gl_08.nif -Decoding meshes\f\flora_tree_gl_01.nif -Decoding meshes\f\flora_tree_gl_03.nif -Decoding meshes\f\flora_tree_gl_02.nif -Decoding meshes\f\flora_tree_gl_05.nif -Decoding meshes\f\flora_tree_gl_04.nif -Decoding meshes\f\flora_tree_gl_07.nif -Decoding meshes\f\flora_tree_gl_06.nif -Decoding meshes\f\flora_heather_01.nif -Decoding meshes\f\flora_bc_knee_04.nif -Decoding meshes\f\flora_bc_knee_01.nif -Decoding meshes\f\flora_bc_knee_03.nif -Decoding meshes\f\flora_bc_knee_02.nif -Decoding meshes\f\flora_tree_01.nif -Decoding meshes\f\flora_kelp_02.nif -Decoding meshes\f\flora_grass_04.nif -Decoding meshes\f\flora_grass_02.nif -Decoding meshes\f\flora_kelp_03.nif -Decoding meshes\f\flora_tree_02.nif -Decoding meshes\f\flora_tree_04.nif -Decoding meshes\f\flora_tree_03.nif -Decoding meshes\f\flora_grass_03.nif -Decoding meshes\f\flora_grass_01.nif -Decoding meshes\f\flora_kelp_01.nif -Decoding meshes\f\flora_bush_01.nif -Decoding meshes\f\flora_kelp_04.nif -Decoding meshes\f\flora_ivy_01.nif -Decoding meshes\f\flora_ivy_02.nif -Decoding meshes\a\a_helm_colovian.nif -Decoding meshes\a\a_helm_silver.nif -Decoding meshes\f\velothi_sewer_door.nif -Decoding meshes\x\terrain_rock_ma_5a.nif -Decoding meshes\x\terrain_rock_ma_39.nif -Decoding meshes\x\terrain_rock_ma_19.nif -Decoding meshes\x\terrain_rock_ma_09.nif -Decoding meshes\x\terrain_rock_ma_79.nif -Decoding meshes\x\terrain_rock_ma_59.nif -Decoding meshes\x\terrain_rock_ma_49.nif -Decoding meshes\x\terrain_rock_ma_58.nif -Decoding meshes\x\terrain_rock_ma_53.nif -Decoding meshes\x\terrain_rock_ma_43.nif -Decoding meshes\x\terrain_rock_ma_02.nif -Decoding meshes\x\terrain_rock_ma_01.nif -Decoding meshes\x\terrain_rock_ma_51.nif -Decoding meshes\x\terrain_rock_ma_10.nif -Decoding meshes\x\terrain_rock_ma_40.nif -Decoding meshes\x\terrain_rock_ma_07.nif -Decoding meshes\x\terrain_rock_ma_67.nif -Decoding meshes\x\terrain_rock_ma_57.nif -Decoding meshes\x\terrain_rock_ma_06.nif -Decoding meshes\x\terrain_rock_ma_56.nif -Decoding meshes\x\terrain_rock_ma_25.nif -Decoding meshes\x\terrain_rock_ma_05.nif -Decoding meshes\x\terrain_rock_ma_55.nif -Decoding meshes\x\terrain_rock_ma_04.nif -Decoding meshes\x\terrain_rock_ma_54.nif -Decoding meshes\x\terrain_ashmire_01.nif -Decoding meshes\x\terrain_ma_rock_51.nif -Decoding meshes\x\terrain_ma_rock_53.nif -Decoding meshes\x\terrain_ma_rock_13.nif -Decoding meshes\x\terrain_rock_ma31.nif -Decoding meshes\x\terrain_rock_ma41.nif -Decoding meshes\x\terrain_ma_rock13.nif -Decoding meshes\x\terrain_ma_bridge.nif -Decoding meshes\x\terrain_ma_rock22.nif -Decoding meshes\x\terrain_ma_rock32.nif -Decoding meshes\x\terrain_ma_rock62.nif -Decoding meshes\x\terrain_lavapot_02.nif -Decoding meshes\x\terrain_lavapot_03.nif -Decoding meshes\x\terrain_lava_vent.nif -Decoding meshes\x\furn_sign_arms_01.nif -Decoding meshes\x\furn_tikitorch_out.nif -Decoding meshes\x\furn_com_fence_02.nif -Decoding meshes\x\furn_com_fence_03.nif -Decoding meshes\x\furn_com_fence_01.nif -Decoding meshes\editormarker_box_01.nif -Decoding meshes\x\terrain_lavapot.nif -Decoding meshes\x\terrain_lava_pot.nif -Decoding meshes\x\terrain_volcano.nif -Decoding meshes\x\terrain_ma_60.nif -Decoding meshes\x\terrain_ma_20.nif -Decoding meshes\x\terrain_ma_30.nif -Decoding meshes\x\furn_sign_inn_01.nif -Decoding meshes\x\furn_flagpole_01.nif -Decoding meshes\x\furn_signbase_01.nif -Decoding meshes\e\frost_shield.nif -Decoding meshes\l\light_com_torch_01.nif -Decoding meshes\l\light_com_torch_02.nif -Decoding meshes\l\light_tikitorch00.nif -Decoding meshes\l\light_de_candle_08.nif -Decoding meshes\l\light_de_candle_18.nif -Decoding meshes\l\light_de_candle_09.nif -Decoding meshes\l\light_de_candle_19.nif -Decoding meshes\l\light_de_candle_24.nif -Decoding meshes\l\light_de_candle_04.nif -Decoding meshes\l\light_de_candle_14.nif -Decoding meshes\l\light_de_candle_25.nif -Decoding meshes\l\light_de_candle_05.nif -Decoding meshes\l\light_de_candle_15.nif -Decoding meshes\l\light_de_candle_26.nif -Decoding meshes\l\light_de_candle_06.nif -Decoding meshes\l\light_de_candle_16.nif -Decoding meshes\l\light_de_candle_07.nif -Decoding meshes\l\light_de_candle_17.nif -Decoding meshes\l\light_de_candle_20.nif -Decoding meshes\l\light_de_candle_10.nif -Decoding meshes\l\light_de_candle_21.nif -Decoding meshes\l\light_de_candle_01.nif -Decoding meshes\l\light_de_candle_11.nif -Decoding meshes\l\light_de_candle_22.nif -Decoding meshes\l\light_de_candle_02.nif -Decoding meshes\l\light_de_candle_12.nif -Decoding meshes\l\light_de_candle_23.nif -Decoding meshes\l\light_de_candle_03.nif -Decoding meshes\l\light_de_candle_13.nif -Decoding meshes\l\light_com_lamp_01.nif -Decoding meshes\l\light_com_lamp_02.nif -Decoding meshes\l\light_redware_lamp.nif -Decoding meshes\l\light_6th_brazier.nif -Decoding meshes\l\light_fire_nosmoke.nif -Decoding meshes\l\light_dwrv_neon00.nif -Decoding meshes\d\door_dwrv_double01.nif -Decoding meshes\d\door_dwrv_double00.nif -Decoding meshes\d\door_dwrv_loadup00.nif -Decoding meshes\d\door_dwrv_inner00.nif -Decoding meshes\d\door_dwrv_load00.nif -Decoding meshes\d\door_dwrv_main00.nif -Decoding meshes\l\light_dae_censer.nif -Decoding meshes\l\light_common_01.nif -Decoding meshes\l\light_logpile10.nif -Decoding meshes\l\light_pitfire00.nif -Decoding meshes\l\light_buglamp_01.nif -Decoding meshes\l\light_de_lamp_01.nif -Decoding meshes\l\light_de_lamp_03.nif -Decoding meshes\l\light_de_lamp_02.nif -Decoding meshes\l\light_de_lamp_05.nif -Decoding meshes\l\light_de_lamp_04.nif -Decoding meshes\l\light_de_lamp_07.nif -Decoding meshes\l\light_de_lamp_06.nif -Decoding meshes\l\light_de_lamp_09.nif -Decoding meshes\l\light_de_lamp_08.nif -Decoding meshes\l\light_dae_jet.nif -Decoding meshes\l\light_stand_01.nif -Decoding meshes\l\light_tikilamp.nif -Decoding meshes\l\light_torch10.nif -Decoding meshes\l\light_sconce10.nif -Decoding meshes\l\light_sconce00.nif -Decoding meshes\l\light_torch_01.nif -Decoding meshes\b\b_n_breton_m_skins.nif -Decoding meshes\b\b_n_breton_f_skins.nif -Decoding meshes\b\b_n_breton_f_ankle.nif -Decoding meshes\b\b_n_breton_m_ankle.nif -Decoding meshes\b\b_n_breton_m_groin.nif -Decoding meshes\b\b_n_breton_f_groin.nif -Decoding meshes\b\b_n_breton_f_wrist.nif -Decoding meshes\b\b_n_breton_m_wrist.nif -Decoding meshes\b\b_n_breton_f_neck.nif -Decoding meshes\b\b_n_breton_m_neck.nif -Decoding meshes\b\b_n_breton_f_foot.nif -Decoding meshes\b\b_n_breton_m_foot.nif -Decoding meshes\b\b_n_breton_f_knee.nif -Decoding meshes\b\b_n_breton_m_knee.nif -Decoding meshes\b\b_n_orc_f_forearm.nif -Decoding meshes\b\b_n_orc_m_forearm.nif -Decoding meshes\b\b_n_orc_m_head_04.nif -Decoding meshes\b\b_n_orc_m_head_01.nif -Decoding meshes\b\b_n_orc_m_head_02.nif -Decoding meshes\b\b_n_orc_m_head_03.nif -Decoding meshes\b\b_n_orc_m_hair_04.nif -Decoding meshes\b\b_n_orc_m_hair_05.nif -Decoding meshes\b\b_n_orc_m_hair_01.nif -Decoding meshes\b\b_n_orc_m_hair_02.nif -Decoding meshes\b\b_n_orc_m_hair_03.nif -Decoding meshes\b\b_n_orc_f_head_01.nif -Decoding meshes\b\b_n_orc_f_head_02.nif -Decoding meshes\b\b_n_orc_f_head_03.nif -Decoding meshes\a\a_m_chitin_a_boot.nif -Decoding meshes\a\a_m_chitin_skinned.nif -Decoding meshes\a\a_m_chitin_forearm.nif -Decoding meshes\a\a_m_chitin_f_boots.nif -Decoding meshes\a\a_m_chitin_helmet.nif -Decoding meshes\b\b_n_khajiit_m_knee.nif -Decoding meshes\b\b_n_khajiit_f_knee.nif -Decoding meshes\b\b_n_khajiit_m_neck.nif -Decoding meshes\b\b_n_khajiit_f_neck.nif -Decoding meshes\b\b_n_nord_m_hair03.nif -Decoding meshes\b\b_n_nord_m_hair02.nif -Decoding meshes\b\b_n_nord_m_hair01.nif -Decoding meshes\b\b_n_nord_m_hair00.nif -Decoding meshes\b\b_n_nord_m_hair07.nif -Decoding meshes\b\b_n_nord_m_hair06.nif -Decoding meshes\b\b_n_nord_m_hair05.nif -Decoding meshes\b\b_n_nord_m_hair04.nif -Decoding meshes\b\b_n_nord_f_head_01.nif -Decoding meshes\b\b_n_nord_m_head_01.nif -Decoding meshes\b\b_n_nord_m_head_02.nif -Decoding meshes\b\b_n_nord_f_head_03.nif -Decoding meshes\b\b_n_nord_m_head_03.nif -Decoding meshes\b\b_n_nord_f_head_02.nif -Decoding meshes\b\b_n_nord_m_head_04.nif -Decoding meshes\b\b_n_nord_f_head_05.nif -Decoding meshes\b\b_n_nord_m_head_05.nif -Decoding meshes\b\b_n_nord_f_head_04.nif -Decoding meshes\b\b_n_nord_m_head_06.nif -Decoding meshes\b\b_n_nord_f_head_07.nif -Decoding meshes\b\b_n_nord_m_head_07.nif -Decoding meshes\b\b_n_nord_f_head_06.nif -Decoding meshes\b\b_n_nord_m_head_08.nif -Decoding meshes\b\b_n_nord_f_head_08.nif -Decoding meshes\b\b_n_nord_f_hair_03.nif -Decoding meshes\b\b_n_nord_f_hair_02.nif -Decoding meshes\b\b_n_nord_f_hair_01.nif -Decoding meshes\b\b_n_nord_f_hair_05.nif -Decoding meshes\b\b_n_nord_f_hair_04.nif -Decoding meshes\b\b_n_nord_f_forearm.nif -Decoding meshes\b\b_n_nord_m_forearm.nif -Decoding meshes\cursor_drop_ground.nif -Decoding meshes\a\a_moragtong_helm.nif -Decoding meshes\b\b_n_nord_f_foot.nif -Decoding meshes\b\b_n_nord_m_ankle.nif -Decoding meshes\b\b_n_nord_m_foot.nif -Decoding meshes\b\b_n_nord_f_ankle.nif -Decoding meshes\b\b_n_nord_m_knee.nif -Decoding meshes\b\b_n_nord_f_knee.nif -Decoding meshes\b\b_n_nord_m_skins.nif -Decoding meshes\b\b_n_nord_m_neck.nif -Decoding meshes\b\b_n_nord_f_skins.nif -Decoding meshes\b\b_n_nord_m_groin.nif -Decoding meshes\b\b_n_nord_f_neck.nif -Decoding meshes\b\b_n_nord_f_wrist.nif -Decoding meshes\b\b_n_nord_m_wrist.nif -Decoding meshes\b\b_n_nord_f_groin.nif -Decoding meshes\b\b_n_orc_m_wrist.nif -Decoding meshes\b\b_n_orc_f_groin.nif -Decoding meshes\b\b_n_orc_m_groin.nif -Decoding meshes\b\b_n_orc_f_hair03.nif -Decoding meshes\b\b_n_orc_f_hair02.nif -Decoding meshes\b\b_n_orc_f_hair01.nif -Decoding meshes\b\b_n_orc_f_hair05.nif -Decoding meshes\b\b_n_orc_f_hair04.nif -Decoding meshes\b\b_n_orc_f_wrist.nif -Decoding meshes\b\b_n_orc_m_skins.nif -Decoding meshes\b\b_n_orc_f_skins.nif -Decoding meshes\b\b_n_orc_f_ankle.nif -Decoding meshes\b\b_n_orc_m_ankle.nif -Decoding meshes\a\a_molecrab_helm.nif -Decoding meshes\b\b_n_orc_f_knee.nif -Decoding meshes\b\b_n_orc_f_neck.nif -Decoding meshes\b\b_n_orc_f_foot.nif -Decoding meshes\b\b_n_orc_m_foot.nif -Decoding meshes\b\b_n_orc_m_knee.nif -Decoding meshes\b\b_n_orc_m_neck.nif -Decoding meshes\chimney_smoke_green.nif -Decoding meshes\chimney_smoke_small.nif -Decoding meshes\chimney_smoke02.nif -Decoding meshes\a\a_orcish_greaves_g.nif -Decoding meshes\a\a_orcish_greaves_k.nif -Decoding meshes\a\a_orcish_bracer_w.nif -Decoding meshes\a\a_orcish_cuirass_c.nif -Decoding meshes\a\a_orcish_boots_gnd.nif -Decoding meshes\c\c_m_robe_common_3.nif -Decoding meshes\c\c_m_robe_common_4.nif -Decoding meshes\c\c_m_robe_common_5.nif -Decoding meshes\c\c_m_robe_extrav_1c.nif -Decoding meshes\c\c_m_robe_extrav_1b.nif -Decoding meshes\c\c_m_robe_extrav_1a.nif -Decoding meshes\c\c_m_robe_extrav_1h.nif -Decoding meshes\c\c_m_robe_extrav_1t.nif -Decoding meshes\c\c_m_robe_extrav_1r.nif -Decoding meshes\c\c_m_robe_common_01.nif -Decoding meshes\c\c_m_robe_common_02.nif -Decoding meshes\c\c_m_robe_extrav_2.nif -Decoding meshes\c\c_m_robe_extrav_1.nif -Decoding meshes\c\c_m_robe_expens_3.nif -Decoding meshes\m\pick_apprentice_01.nif -Decoding meshes\m\pick_journeyman_01.nif -Decoding meshes\m\pick_master_01.nif -Decoding meshes\a\a_orcish_helmet.nif -Decoding meshes\a\a_orcish_boots_f.nif -Decoding meshes\a\a_orcish_boots_a.nif -Decoding meshes\a\a_newstscale_c_gnd.nif -Decoding meshes\a\a_netch_m_cuirass2.nif -Decoding meshes\a\a_netch_m_boot_gnd.nif -Decoding meshes\a\a_netch_m_greave_g.nif -Decoding meshes\a\a_netch_m_skinned.nif -Decoding meshes\a\a_nordicfur_boot_f.nif -Decoding meshes\a\a_nordicfur_boot_a.nif -Decoding meshes\a\a_nordiciron_helm.nif -Decoding meshes\a\a_nordicfur_helmet.nif -Decoding meshes\a\a_nordiciron_c_gnd.nif -Decoding meshes\argonian_swimkna.nif -Decoding meshes\a\a_netch_m_helmet.nif -Decoding meshes\a\a_netch_m_boot_f.nif -Decoding meshes\a\a_netch_m_boot_a.nif -Decoding meshes\a\a_nordiciron_c.nif -Decoding meshes\a\a_apostle_boots_f.nif -Decoding meshes\o\flora_saltrice_02.nif -Decoding meshes\o\flora_saltrice_01.nif -Decoding meshes\o\flora_fire_fern_01.nif -Decoding meshes\o\flora_fire_fern_02.nif -Decoding meshes\o\flora_fire_fern_03.nif -Decoding meshes\o\flora_chokeweed_01.nif -Decoding meshes\o\flora_roobrush_01.nif -Decoding meshes\o\flora_wickwheat_03.nif -Decoding meshes\o\flora_wickwheat_02.nif -Decoding meshes\o\flora_wickwheat_01.nif -Decoding meshes\o\flora_wickwheat_04.nif -Decoding meshes\o\flora_kreshweed_03.nif -Decoding meshes\o\flora_kreshweed_02.nif -Decoding meshes\o\flora_kreshweed_01.nif -Decoding meshes\o\flora_hackle-lo_01.nif -Decoding meshes\o\flora_hackle-lo_02.nif -Decoding meshes\a\a_art_wraithguard.nif -Decoding meshes\box.nif -Decoding meshes\r\corprus_stalker.nif -Decoding meshes\o\flora_ash_yam_02.nif -Decoding meshes\o\flora_ash_yam_01.nif -Decoding meshes\r\xgreatbonewalker.nif -Decoding meshes\r\atronach_fire.nif -Decoding meshes\r\atronach_storm.nif -Decoding meshes\r\atronach_frost.nif -Decoding meshes\a\a_art_dragon_gnd.nif -Decoding meshes\w\w_wakizashi_iron.nif -Decoding meshes\w\w_waraxe_daedric.nif -Decoding meshes\w\w_warhammer_iron.nif -Decoding meshes\w\w_waraxe_glass.nif -Decoding meshes\w\w_waraxe_iron.nif -Decoding meshes\w\w_waraxe_ebony.nif -Decoding meshes\w\w_waraxe_steel.nif -Decoding meshes\w\w_wooden_staff.nif -Decoding meshes\w\w_wakizashi.nif -Decoding meshes\w\w_warhammer.nif -Decoding meshes\c\c_belt_exquisite_1.nif -Decoding meshes\c\c_belt_expensive_1.nif -Decoding meshes\c\c_belt_expensive_3.nif -Decoding meshes\c\c_belt_expensive_2.nif -Decoding meshes\o\contain_drawer_02.nif -Decoding meshes\o\contain_drawer_03.nif -Decoding meshes\o\contain_drawer_01.nif -Decoding meshes\o\contain_de_desk_01.nif -Decoding meshes\o\contain_barrel_01.nif -Decoding meshes\o\contain_couldron10.nif -Decoding meshes\o\contain_corpse20.nif -Decoding meshes\o\contain_corpse00.nif -Decoding meshes\o\contain_corpse10.nif -Decoding meshes\o\contain_chest10.nif -Decoding meshes\o\contain_barrel10.nif -Decoding meshes\o\contain_crate_01.nif -Decoding meshes\o\contain_crate_02.nif -Decoding meshes\o\contain_chest11.nif -Decoding meshes\o\contain_sack00.nif -Decoding meshes\o\contain_urn_04.nif -Decoding meshes\o\contain_urn_02.nif -Decoding meshes\o\contain_urn_05.nif -Decoding meshes\o\contain_urn_01.nif -Decoding meshes\o\contain_urn_03.nif -Decoding meshes\o\contain_pot_01.nif -Decoding meshes\r\xascendedsleeper.nif -Decoding meshes\r\xashvampire.nif -Decoding meshes\c\c_belt_common_4.nif -Decoding meshes\c\c_belt_common_2.nif -Decoding meshes\c\c_belt_common_5.nif -Decoding meshes\c\c_belt_common_1.nif -Decoding meshes\c\c_belt_common_3.nif -Decoding meshes\c\c_belt_erabin.nif -Decoding meshes\e\magic_area_conjure.nif -Decoding meshes\e\magic_cast_poison.nif -Decoding meshes\e\magic_cast_conjure.nif -Decoding meshes\e\magic_cast_restore.nif -Decoding meshes\e\magic_cast_fortify.nif -Decoding meshes\e\magic_hit_levitate.nif -Decoding meshes\e\magic_area_poison.nif -Decoding meshes\e\magic_hit_conjure.nif -Decoding meshes\a\a_cephalopod_helm.nif -Decoding meshes\r\xsphere_centurions.nif -Decoding meshes\c\c_art_ring_mentor.nif -Decoding meshes\c\c_art_ring_khajiit.nif -Decoding meshes\c\c_art_ring_warlock.nif -Decoding meshes\e\lightning_shield.nif -Decoding meshes\e\lightningbolts.nif -Decoding meshes\e\lightning_area.nif -Decoding meshes\c\c_m_shirt_expensive_1_u_gnd.nif -Decoding meshes\e\magic_area_rest.nif -Decoding meshes\e\magic_hit_frost.nif -Decoding meshes\e\magic_area_drain.nif -Decoding meshes\e\magic_hit_poison.nif -Decoding meshes\e\magic_area_myst.nif -Decoding meshes\e\magic_cast_myst.nif -Decoding meshes\e\magic_area_frost.nif -Decoding meshes\e\magic_cast_frost.nif -Decoding meshes\e\magic_hit_alter.nif -Decoding meshes\e\magic_hit_ill.nif -Decoding meshes\e\magic_hit_rest.nif -Decoding meshes\e\magic_hit_dst.nif -Decoding meshes\e\magic_reflect.nif -Decoding meshes\e\magic_area_ill.nif -Decoding meshes\e\magic_area_alt.nif -Decoding meshes\e\magic_cast_ill.nif -Decoding meshes\e\magic_cast_alt.nif -Decoding meshes\e\magic_cast_dst.nif -Decoding meshes\e\magic_area_dst.nif -Decoding meshes\e\magic_hit_myst.nif -Decoding meshes\e\magic_hit_s.nif -Decoding meshes\e\magic_cast_l.nif -Decoding meshes\e\magic_cast_s.nif -Decoding meshes\e\magic_summon.nif -Decoding meshes\m\probe_master_01.nif -Decoding meshes\c\c_art_ring_wind.nif -Decoding meshes\a\a_bonemold_boots_a.nif -Decoding meshes\a\a_bonemold_boots_f.nif -Decoding meshes\a\a_bonemold_helmet.nif -Decoding meshes\w\w_tanto_daedric.nif -Decoding meshes\w\w_tanto_iron.nif -Decoding meshes\w\w_shortbow_chitin.nif -Decoding meshes\w\w_shortsword_ebony.nif -Decoding meshes\w\w_silver_claymore.nif -Decoding meshes\c\c_glove_expensive1.nif -Decoding meshes\c\c_glove_moragtong.nif -Decoding meshes\w\w_steel_battleaxe.nif -Decoding meshes\a\a_ebony_greaves_ul.nif -Decoding meshes\a\a_ebony_greaves_k.nif -Decoding meshes\a\a_ebony_g_greaves.nif -Decoding meshes\r\cavemudcrab.nif -Decoding meshes\w\w_staff_daedric.nif -Decoding meshes\w\w_spear_daedric.nif -Decoding meshes\w\w_silver_dagger.nif -Decoding meshes\w\w_silver_waraxe.nif -Decoding meshes\w\w_shortbow_steel.nif -Decoding meshes\a\a_ebony_cuirass.nif -Decoding meshes\a\a_ebony_bracer_w.nif -Decoding meshes\a\a_ebony_boot_gnd.nif -Decoding meshes\c\c_glove_common1.nif -Decoding meshes\w\w_staff_ebony.nif -Decoding meshes\w\w_steel_arrow.nif -Decoding meshes\w\w_steel_star .nif -Decoding meshes\w\w_steel_knife.nif -Decoding meshes\w\w_staff_glass.nif -Decoding meshes\w\w_shortsword00.nif -Decoding meshes\w\w_silver_staff.nif -Decoding meshes\w\w_silver_arrow.nif -Decoding meshes\w\w_silver_star.nif -Decoding meshes\w\w_silver_spear.nif -Decoding meshes\w\w_star_ebony.nif -Decoding meshes\w\w_saber_iron.nif -Decoding meshes\w\w_spikedclub.nif -Decoding meshes\w\w_star_glass.nif -Decoding meshes\w\w_spear_iron.nif -Decoding meshes\a\a_ebony_helmet.nif -Decoding meshes\a\a_ebony_boot_a.nif -Decoding meshes\a\a_ebony_boot_f.nif -Decoding meshes\a\a_daedric_boots_f.nif -Decoding meshes\a\a_daedric_boots_a.nif -Decoding meshes\a\a_dragonscale_helm.nif -Decoding meshes\a\a_dwemer_greaves_g.nif -Decoding meshes\a\a_dwemer_boots_gnd.nif -Decoding meshes\a\a_dwemer_cuirass_c.nif -Decoding meshes\a\a_dwemer_cuir_gnd.nif -Decoding meshes\a\a_dwemer_bracer_w.nif -Decoding meshes\f\xact_banner_khull.nif -Decoding meshes\r\xkwama worker.nif -Decoding meshes\r\xkwama warior.nif -Decoding meshes\r\xkwama forager.nif -Decoding meshes\r\xkwama queen.nif -Decoding meshes\i\in_dae_hall_l_stair_curve_01.nif -Decoding meshes\i\in_dae_hall_l_staircurve_01.nif -Decoding meshes\r\xdwarvenspecter.nif -Decoding meshes\f\xact_banner_vos.nif -Decoding meshes\c\c_m_pants_expensive_1_u_gnd.nif -Decoding meshes\i\in_t_s_hallshaft_ceilingcap.nif -Decoding meshes\a\a_dustadept_helm.nif -Decoding meshes\a\a_dwemer_boots_f.nif -Decoding meshes\a\a_dwemer_boots_a.nif -Decoding meshes\a\a_dwemer_helmet.nif -Decoding meshes\a\a_dreugh_cuirass.nif -Decoding meshes\a\a_daedric_skins.nif -Decoding meshes\a\a_daedric_god_h.nif -Decoding meshes\a\a_dreugh_helm.nif -Decoding meshes\a\a_glass_bracer_gnd.nif -Decoding meshes\a\a_glass_greaves_ul.nif -Decoding meshes\a\a_glass_greaves_g.nif -Decoding meshes\a\a_glass_greaves_k.nif -Decoding meshes\a\a_glass_boots_gnd.nif -Decoding meshes\r\xsteam_centurions.nif -Decoding meshes\r\netch_betty.nif -Decoding meshes\r\xatronach_storm.nif -Decoding meshes\r\xatronach_frost.nif -Decoding meshes\r\xatronach_fire.nif -Decoding meshes\a\a_glass_cuirass.nif -Decoding meshes\a\a_glass_boots_a.nif -Decoding meshes\a\a_glass_bracer_w.nif -Decoding meshes\a\a_glass_boots_f.nif -Decoding meshes\a\a_glass_helmet.nif -Decoding meshes\a\a_fur_cuirass_gnd.nif -Decoding meshes\i\xin_akulakhan00.nif -Decoding meshes\r\xguar_withpack.nif -Decoding meshes\r\xguar_white.nif -Decoding meshes\inventory_window.nif -Decoding meshes\a\a_fur_cuirass.nif -Decoding meshes\w\w_orcish_battleaxe.nif -Decoding meshes\w\w_orcish_warhammer.nif -Decoding meshes\w\w_nordic_claymore.nif -Decoding meshes\w\w_nordic_battleaxe.nif -Decoding meshes\w\w_n_claymore.nif -Decoding meshes\i\test _cavern_i_01.nif -Decoding meshes\r\sphere_centurions.nif -Decoding meshes\n\potion_skooma_01.nif -Decoding meshes\w\w_mace_daedric.nif -Decoding meshes\w\w_mace_ebony.nif -Decoding meshes\w\w_miner_pick.nif -Decoding meshes\w\w_mace_iron.nif -Decoding meshes\w\w_longbow_daedric.nif -Decoding meshes\w\w_longsword_silver.nif -Decoding meshes\w\w_longspear_ebony.nif -Decoding meshes\w\w_longbow_bonemold.nif -Decoding meshes\w\w_longsword_ebony.nif -Decoding meshes\r\xwingedtwilight.nif -Decoding meshes\r\xsiltstrider.nif -Decoding meshes\w\w_longbow_ariel.nif -Decoding meshes\w\w_longbow_steel.nif -Decoding meshes\r\wingedtwilight.nif -Decoding meshes\o\misc_sack00.nif -Decoding meshes\o\misc_chest11.nif -Decoding meshes\r\xancestorghost.nif -Decoding meshes\w\w_katana_daedric.nif -Decoding meshes\w\w_knife_glass.nif -Decoding meshes\w\w_knife_iron.nif -Decoding meshes\i\active_port_valen.nif -Decoding meshes\i\active_port_falen.nif -Decoding meshes\i\active_port_maran.nif -Decoding meshes\i\active_port_beran.nif -Decoding meshes\i\active_port_telas.nif -Decoding meshes\i\active_port_falag.nif -Decoding meshes\i\active_port_falas.nif -Decoding meshes\i\active_dag_port10.nif -Decoding meshes\i\active_port_andra.nif -Decoding meshes\i\in_c_stair_thatch_pend_tall_01.nif -Decoding meshes\i\in_c_stair_thatch_pend_tall_02.nif -Decoding meshes\e\vfx_pattern06.nif -Decoding meshes\e\vfx_pattern07.nif -Decoding meshes\e\vfx_pattern02.nif -Decoding meshes\e\vfx_pattern04.nif -Decoding meshes\e\vfx_pattern03.nif -Decoding meshes\e\vfx_pattern05.nif -Decoding meshes\e\vfx_pattern08.nif -Decoding meshes\m\repair_master_01.nif -Decoding meshes\i\active_port_hlor.nif -Decoding meshes\i\active_port_indo.nif -Decoding meshes\i\active_port_roth.nif -Decoding meshes\r\xcorprus_stalker.nif -Decoding meshes\r\xbonewalker.nif -Decoding meshes\xbase_anim_female.nif -Decoding meshes\xbase_animkna.1st.nif -Decoding meshes\xbase_animkna.nif -Decoding meshes\xbase_anim.1st.nif -Decoding meshes\a\a_m_imperialchain_pauldron_gnd.nif -Decoding meshes\a\a_m_imperialchain_greaves_gnd.nif -Decoding meshes\a\a_m_imperialchain_pauldron_ua.nif -Decoding meshes\a\a_m_imperialchain_greaves_g.nif -Decoding meshes\a\a_m_imperialchain_greaves_ul.nif -Decoding meshes\w\w_iron_shortsword.nif -Decoding meshes\m\misc_foldedcloth00.nif -Decoding meshes\m\misc_de_tankard_01.nif -Decoding meshes\m\misc_de_goblet_09.nif -Decoding meshes\m\misc_de_goblet_08.nif -Decoding meshes\m\misc_de_goblet_01.nif -Decoding meshes\m\misc_de_goblet_03.nif -Decoding meshes\m\misc_de_goblet_02.nif -Decoding meshes\m\misc_de_goblet_05.nif -Decoding meshes\m\misc_de_goblet_04.nif -Decoding meshes\m\misc_de_goblet_07.nif -Decoding meshes\m\misc_de_goblet_06.nif -Decoding meshes\m\misc_dwrv_goblet00.nif -Decoding meshes\m\misc_dwrv_goblet10.nif -Decoding meshes\m\misc_de_pitcher_01.nif -Decoding meshes\m\misc_de_basket_01.nif -Decoding meshes\m\misc_bowl_white_01.nif -Decoding meshes\m\misc_com_bucket_01.nif -Decoding meshes\m\misc_com_pillow_01.nif -Decoding meshes\m\misc_com_basket_02.nif -Decoding meshes\m\misc_com_basket_01.nif -Decoding meshes\m\misc_com_bottle_06.nif -Decoding meshes\m\misc_com_bottle_07.nif -Decoding meshes\m\misc_com_bottle_14.nif -Decoding meshes\m\misc_com_bottle_04.nif -Decoding meshes\m\misc_com_bottle_15.nif -Decoding meshes\m\misc_com_bottle_05.nif -Decoding meshes\m\misc_com_bottle_12.nif -Decoding meshes\m\misc_com_bottle_02.nif -Decoding meshes\m\misc_com_bottle_13.nif -Decoding meshes\m\misc_com_bottle_03.nif -Decoding meshes\m\misc_com_bottle_10.nif -Decoding meshes\m\misc_com_bottle_11.nif -Decoding meshes\m\misc_com_bottle_01.nif -Decoding meshes\m\misc_com_bottle_08.nif -Decoding meshes\m\misc_com_bottle_09.nif -Decoding meshes\m\misc_com_wood_fork.nif -Decoding meshes\m\misc_com_broom_01.nif -Decoding meshes\m\misc_com_plate_06.nif -Decoding meshes\m\misc_com_plate_07.nif -Decoding meshes\m\misc_com_plate_04.nif -Decoding meshes\m\misc_com_plate_05.nif -Decoding meshes\m\misc_com_plate_02.nif -Decoding meshes\m\misc_com_plate_03.nif -Decoding meshes\m\misc_com_plate_01.nif -Decoding meshes\m\misc_com_plate_08.nif -Decoding meshes\m\misc_wheatbundle00.nif -Decoding meshes\m\misc_uni_pillow_02.nif -Decoding meshes\m\misc_redware_bowl.nif -Decoding meshes\m\misc_redware_flask.nif -Decoding meshes\m\misc_rollingpin_01.nif -Decoding meshes\m\misc_redware_vase.nif -Decoding meshes\m\misc_redware_plate.nif -Decoding meshes\m\misc_redware_lamp.nif -Decoding meshes\m\misc_soulgem_grand.nif -Decoding meshes\m\misc_soulgem_petty.nif -Decoding meshes\m\misc_pot_green_01.nif -Decoding meshes\m\misc_portal_shard.nif -Decoding meshes\f\xex_ashl_z_banner.nif -Decoding meshes\f\xex_ashl_a_banner.nif -Decoding meshes\f\xex_ashl_u_banner.nif -Decoding meshes\f\xex_ashl_e_banner.nif -Decoding meshes\l\furn_de_firepit_f.nif -Decoding meshes\r\golden saint.nif -Decoding meshes\m\misc_scrapwood01.nif -Decoding meshes\m\misc_scrapwood03.nif -Decoding meshes\m\misc_scrapwood02.nif -Decoding meshes\m\misc_scrapwood05.nif -Decoding meshes\m\misc_scrapwood04.nif -Decoding meshes\m\misc_lw_platter.nif -Decoding meshes\m\misc_dwrv_bowl00.nif -Decoding meshes\m\misc_de_drum_02.nif -Decoding meshes\m\misc_shackles00.nif -Decoding meshes\m\misc_redware_cup.nif -Decoding meshes\m\misc_ropecoil00.nif -Decoding meshes\m\misc_wickwheat00.nif -Decoding meshes\m\misc_de_bowl_01.nif -Decoding meshes\m\misc_pot_blue_01.nif -Decoding meshes\m\misc_pot_blue_02.nif -Decoding meshes\m\misc_de_lute_01.nif -Decoding meshes\m\misc_dwrv_coin00.nif -Decoding meshes\m\misc_de_drum_01.nif -Decoding meshes\m\misc_6th_goblet.nif -Decoding meshes\m\misc_dwrv_gear00.nif -Decoding meshes\m\misc_placemat_01.nif -Decoding meshes\m\misc_dwrv_mug00.nif -Decoding meshes\r\siltstrider.nif -Decoding meshes\m\misc_flask_02.nif -Decoding meshes\m\misc_hammer10.nif -Decoding meshes\m\misc_prongs00.nif -Decoding meshes\m\misc_shears_01.nif -Decoding meshes\m\misc_beaker_01.nif -Decoding meshes\m\misc_spool_01.nif -Decoding meshes\m\misc_flask_01.nif -Decoding meshes\m\misc_bellows10.nif -Decoding meshes\m\misc_flask_04.nif -Decoding meshes\m\misc_lw_flask.nif -Decoding meshes\m\misc_flask_03.nif -Decoding meshes\m\misc_lw_cup.nif -Decoding meshes\m\misc_cloth10.nif -Decoding meshes\m\misc_lw_bowl.nif -Decoding meshes\m\misc_inkwell.nif -Decoding meshes\m\misc_cloth11.nif -Decoding meshes\m\misc_skull00.nif -Decoding meshes\m\misc_skull10.nif -Decoding meshes\r\xclannfear_daddy.nif -Decoding meshes\r\xcliffracer.nif -Decoding meshes\steam_bluegreen.nif -Decoding meshes\steam_lavariver.nif -Decoding meshes\l\furn_de_chair_02.nif -Decoding meshes\r\xslaughterfish.nif -Decoding meshes\w\w_iron_claymore.nif -Decoding meshes\w\w_iron_longsword.nif -Decoding meshes\w\w_iron_dagger.nif -Decoding meshes\w\w_iron_arrow.nif -Decoding meshes\sky_moon_small.nif -Decoding meshes\sky_moon_large.nif -Decoding meshes\l\misc_candle_red_01.nif -Decoding meshes\r\lame_corprus.nif -Decoding meshes\w\w_halberd_glass.nif -Decoding meshes\w\w_halberd_steel.nif -Decoding meshes\w\w_halberd_iron.nif -Decoding meshes\c\c_shirt_aralor_fa.nif -Decoding meshes\c\c_shirt_aralor_ua.nif -Decoding meshes\c\c_shirt_aralor_gnd.nif -Decoding meshes\sky_clouds_01_no_tex.nif -Decoding meshes\w\magic_target_myst.nif -Decoding meshes\w\magic_target_rest.nif -Decoding meshes\w\magic_target_frost.nif -Decoding meshes\c\c_shoes_extrav_2_f.nif -Decoding meshes\c\c_shoes_extrav_1_f.nif -Decoding meshes\c\c_shoes_rilms_gnd.nif -Decoding meshes\e\soultraphit.nif -Decoding meshes\w\magic_target_ill.nif -Decoding meshes\w\magic_target_dst.nif -Decoding meshes\w\magic_target_alt.nif -Decoding meshes\w\magic_target.nif -Decoding meshes\c\c_shoes_common_3.nif -Decoding meshes\c\c_shoes_common_4.nif -Decoding meshes\c\c_shoes_common_5.nif -Decoding meshes\c\c_shirt_aralor_c.nif -Decoding meshes\c\c_shirt_aralor_w.nif -Decoding meshes\c\c_skirt_common_5.nif -Decoding meshes\c\c_skirt_common_2.nif -Decoding meshes\c\c_skirt_common_3.nif -Decoding meshes\w\w_glass_arrow.nif -Decoding meshes\sky_clouds_01.nif -Decoding meshes\c\c_slave_bracer.nif -Decoding meshes\c\c_shoes_rilms.nif -Decoding meshes\r\g_centurionspider.nif -Decoding meshes\c\c_ring_exquisite_1.nif -Decoding meshes\c\c_ring_expensive_1.nif -Decoding meshes\c\c_ring_expensive_3.nif -Decoding meshes\c\c_ring_expensive_2.nif -Decoding meshes\r\ancestorghost.nif -Decoding meshes\r\ascendedsleeper.nif -Decoding meshes\r\xscamp_fetch.nif -Decoding meshes\c\c_ring_common05.nif -Decoding meshes\c\c_ring_common01.nif -Decoding meshes\c\c_ring_common03.nif -Decoding meshes\c\c_ring_moonnstar.nif -Decoding meshes\c\c_ring_common04.nif -Decoding meshes\c\c_ring_common02.nif -Decoding meshes\c\c_ring_namira.nif -Decoding meshes\a\a_silver_duke_gnd.nif -Decoding meshes\a\a_silver_cuir_gnd.nif -Decoding meshes\a\a_steel_greaves_ul.nif -Decoding meshes\a\a_steel_greaves_g.nif -Decoding meshes\a\a_steel_greaves_k.nif -Decoding meshes\a\a_steel_hands.1st.nif -Decoding meshes\a\a_steel_boots_gnd.nif -Decoding meshes\a\a_shield_imperial.nif -Decoding meshes\f\active_de_bar_door.nif -Decoding meshes\f\active_akhul_steam.nif -Decoding meshes\f\active_signpost_01.nif -Decoding meshes\f\active_signpost_02.nif -Decoding meshes\f\active_de_bedroll.nif -Decoding meshes\f\act_banner_hla_oad.nif -Decoding meshes\f\act_banner_tel_fyr.nif -Decoding meshes\f\act_banner_tel_vos.nif -Decoding meshes\f\active_de_bed_19.nif -Decoding meshes\f\active_de_bed_18.nif -Decoding meshes\f\active_de_bed_13.nif -Decoding meshes\f\active_de_bed_12.nif -Decoding meshes\f\active_de_bed_11.nif -Decoding meshes\f\active_de_bed_10.nif -Decoding meshes\f\active_de_bed_17.nif -Decoding meshes\f\active_de_bed_16.nif -Decoding meshes\f\active_de_bed_15.nif -Decoding meshes\f\active_de_bed_14.nif -Decoding meshes\f\active_de_bed_09.nif -Decoding meshes\f\active_de_bed_08.nif -Decoding meshes\f\active_de_bed_03.nif -Decoding meshes\f\active_de_bed_02.nif -Decoding meshes\f\active_de_bed_01.nif -Decoding meshes\f\active_de_bed_07.nif -Decoding meshes\f\active_de_bed_06.nif -Decoding meshes\f\active_de_bed_05.nif -Decoding meshes\f\active_de_bed_04.nif -Decoding meshes\f\active_de_bed_30.nif -Decoding meshes\f\active_de_bed_29.nif -Decoding meshes\f\active_de_bed_28.nif -Decoding meshes\f\active_de_bed_23.nif -Decoding meshes\f\active_de_bed_22.nif -Decoding meshes\f\active_de_bed_21.nif -Decoding meshes\f\active_de_bed_20.nif -Decoding meshes\f\active_de_bed_27.nif -Decoding meshes\f\active_de_bed_26.nif -Decoding meshes\f\active_de_bed_25.nif -Decoding meshes\f\active_de_bed_24.nif -Decoding meshes\f\active_bubbles00.nif -Decoding meshes\f\active_button_01.nif -Decoding meshes\f\act_banner_khull.nif -Decoding meshes\f\act_banner_vos.nif -Decoding meshes\f\active_gong_01.nif -Decoding meshes\a\a_silver_cuirass.nif -Decoding meshes\w\w_ebony_arrow.nif -Decoding meshes\vfx_defaultcast.nif -Decoding meshes\vfx_defaultarea.nif -Decoding meshes\vfx_defaulthit.nif -Decoding meshes\sky_atmosphere.nif -Decoding meshes\a\a_steel_helmet.nif -Decoding meshes\a\a_steel_boot_f.nif -Decoding meshes\a\a_steel_boot_a.nif -Decoding meshes\a\a_steel_skin.nif -Decoding meshes\f\xfurn_imp_flag_01.nif -Decoding meshes\a\a_ringmail_cuirass.nif -Decoding meshes\w\w_dwemer_warhammer.nif -Decoding meshes\w\w_dwemer_claymore.nif -Decoding meshes\w\w_dwemer_battleaxe.nif -Decoding meshes\w\w_dwemer_longspear.nif -Decoding meshes\a\a_redoranmaster_h.nif -Decoding meshes\f\sound_dummy00.nif -Decoding meshes\r\clannfear_daddy.nif -Decoding meshes\r\slaughterfish.nif -Decoding meshes\r\xlame_corprus.nif -Decoding meshes\r\heart_akulakhan.nif -Decoding meshes\r\leastkagouti.nif -Decoding meshes\r\scamp_fetch.nif -Decoding meshes\r\xcavemudcrab.nif -Decoding meshes\r\dwarvenspecter.nif -Decoding meshes\r\kwama forager.nif -Decoding meshes\r\kwama worker.nif -Decoding meshes\r\kwama queen.nif -Decoding meshes\r\kwama warior.nif -Decoding meshes\r\guar_withpack.nif -Decoding meshes\w\w_dwemer_waraxe.nif -Decoding meshes\w\w_dwemer_halberd.nif -Decoding meshes\w\w_daedric_arrow.nif -Decoding meshes\w\w_dagger_dragon.nif -Decoding meshes\w\w_dagger_daedric.nif -Decoding meshes\w\w_dagger_chitin.nif -Decoding meshes\w\w_dwemer_spear.nif -Decoding meshes\w\w_dwemer_mace.nif -Decoding meshes\w\w_dreugh_staff.nif -Decoding meshes\w\w_dreugh_club.nif -Decoding meshes\w\w_dagger_glass.nif -Decoding meshes\w\w_dart_silver.nif -Decoding meshes\w\w_dart_daedric.nif -Decoding meshes\w\w_dart_ebony.nif -Decoding meshes\w\w_dart_steel.nif -Decoding meshes\w\w_daikatana.nif -Decoding meshes\b\b_v_orc_m_head_01.nif -Decoding meshes\b\b_v_orc_f_head_01.nif -Decoding meshes\w\w_crossbow_dwemer.nif -Decoding meshes\w\w_claymore_crystal.nif -Decoding meshes\w\w_claymore_daedric.nif -Decoding meshes\b\b_v_nord_f_head_01.nif -Decoding meshes\b\b_v_nord_m_head_01.nif -Decoding meshes\f\terrain_bc_scum_01.nif -Decoding meshes\f\terrain_bc_scum_03.nif -Decoding meshes\f\terrain_bc_scum_02.nif -Decoding meshes\f\terrain_rock_ac_08.nif -Decoding meshes\f\terrain_rock_bc_08.nif -Decoding meshes\f\terrain_rock_bc_18.nif -Decoding meshes\f\terrain_rock_ac_09.nif -Decoding meshes\f\terrain_rock_bc_09.nif -Decoding meshes\f\terrain_rock_ac_02.nif -Decoding meshes\f\terrain_rock_ac_12.nif -Decoding meshes\f\terrain_rock_bc_02.nif -Decoding meshes\f\terrain_rock_bc_12.nif -Decoding meshes\f\terrain_rock_ac_03.nif -Decoding meshes\f\terrain_rock_bc_03.nif -Decoding meshes\f\terrain_rock_bc_13.nif -Decoding meshes\f\terrain_rock_ac_10.nif -Decoding meshes\f\terrain_rock_bc_10.nif -Decoding meshes\f\terrain_rock_ac_01.nif -Decoding meshes\f\terrain_rock_ac_11.nif -Decoding meshes\f\terrain_rock_bc_01.nif -Decoding meshes\f\terrain_rock_bc_11.nif -Decoding meshes\f\terrain_rock_ac_06.nif -Decoding meshes\f\terrain_rock_bc_06.nif -Decoding meshes\f\terrain_rock_bc_16.nif -Decoding meshes\f\terrain_rock_ac_07.nif -Decoding meshes\f\terrain_rock_bc_07.nif -Decoding meshes\f\terrain_rock_bc_17.nif -Decoding meshes\f\terrain_rock_ac_04.nif -Decoding meshes\f\terrain_rock_bc_04.nif -Decoding meshes\f\terrain_rock_bc_14.nif -Decoding meshes\f\terrain_rock_ac_05.nif -Decoding meshes\f\terrain_rock_bc_05.nif -Decoding meshes\f\terrain_rock_bc_15.nif -Decoding meshes\f\terrain_rock_ai_09.nif -Decoding meshes\f\terrain_rock_ai_08.nif -Decoding meshes\f\terrain_rock_ai_01.nif -Decoding meshes\f\terrain_rock_ai_11.nif -Decoding meshes\f\terrain_rock_ai_10.nif -Decoding meshes\f\terrain_rock_ai_03.nif -Decoding meshes\f\terrain_rock_ai_02.nif -Decoding meshes\f\terrain_rock_ai_12.nif -Decoding meshes\f\terrain_rock_ai_05.nif -Decoding meshes\f\terrain_rock_ai_04.nif -Decoding meshes\f\terrain_rock_ai_07.nif -Decoding meshes\f\terrain_rock_ai_06.nif -Decoding meshes\f\terrain_rock_wg_09.nif -Decoding meshes\f\terrain_rock_wg_18.nif -Decoding meshes\f\terrain_rock_wg_08.nif -Decoding meshes\f\terrain_rock_wg_15.nif -Decoding meshes\f\terrain_rock_wg_05.nif -Decoding meshes\f\terrain_rock_wg_14.nif -Decoding meshes\f\terrain_rock_wg_04.nif -Decoding meshes\f\terrain_rock_wg_17.nif -Decoding meshes\f\terrain_rock_wg_07.nif -Decoding meshes\f\terrain_rock_wg_16.nif -Decoding meshes\f\terrain_rock_wg_06.nif -Decoding meshes\f\terrain_rock_wg_11.nif -Decoding meshes\f\terrain_rock_wg_01.nif -Decoding meshes\f\terrain_rock_wg_10.nif -Decoding meshes\f\terrain_rock_wg_13.nif -Decoding meshes\f\terrain_rock_wg_03.nif -Decoding meshes\f\terrain_rock_wg_12.nif -Decoding meshes\f\terrain_rock_wg_02.nif -Decoding meshes\f\terrain_rock_ma_01.nif -Decoding meshes\f\terrain_boulder_02.nif -Decoding meshes\f\terrain_boulder_03.nif -Decoding meshes\f\terrain_boulder_01.nif -Decoding meshes\f\terrain_boulder_04.nif -Decoding meshes\f\terrain_boulder_05.nif -Decoding meshes\f\terrain_rock_gl_03.nif -Decoding meshes\f\terrain_rock_gl_02.nif -Decoding meshes\f\terrain_rock_gl_12.nif -Decoding meshes\f\terrain_rock_gl_01.nif -Decoding meshes\f\terrain_rock_gl_11.nif -Decoding meshes\f\terrain_rock_gl_10.nif -Decoding meshes\f\terrain_rock_gl_07.nif -Decoding meshes\f\terrain_rock_gl_06.nif -Decoding meshes\f\terrain_rock_gl_05.nif -Decoding meshes\f\terrain_rock_gl_04.nif -Decoding meshes\f\terrain_rock_gl_09.nif -Decoding meshes\f\terrain_rock_gl_08.nif -Decoding meshes\f\terrain_rock_rm_09.nif -Decoding meshes\f\terrain_rock_rm_19.nif -Decoding meshes\f\terrain_rock_rm_08.nif -Decoding meshes\f\terrain_rock_rm_18.nif -Decoding meshes\f\terrain_rock_rm_07.nif -Decoding meshes\f\terrain_rock_rm_17.nif -Decoding meshes\f\terrain_rock_rm_06.nif -Decoding meshes\f\terrain_rock_rm_16.nif -Decoding meshes\f\terrain_rock_rm_05.nif -Decoding meshes\f\terrain_rock_rm_15.nif -Decoding meshes\f\terrain_rock_rm_04.nif -Decoding meshes\f\terrain_rock_rm_14.nif -Decoding meshes\f\terrain_rock_rm_24.nif -Decoding meshes\f\terrain_rock_rm_03.nif -Decoding meshes\f\terrain_rock_rm_13.nif -Decoding meshes\f\terrain_rock_rm_23.nif -Decoding meshes\f\terrain_rock_rm_02.nif -Decoding meshes\f\terrain_rock_rm_12.nif -Decoding meshes\f\terrain_rock_rm_22.nif -Decoding meshes\f\terrain_rock_rm_01.nif -Decoding meshes\f\terrain_rock_rm_11.nif -Decoding meshes\f\terrain_rock_rm_21.nif -Decoding meshes\f\terrain_rock_rm_10.nif -Decoding meshes\f\terrain_rock_rm_20.nif -Decoding meshes\f\furn_roped_pole_01.nif -Decoding meshes\f\furn_rail_broke00.nif -Decoding meshes\f\furn_rail_elbow_00.nif -Decoding meshes\f\furn_rail_slope_00.nif -Decoding meshes\f\furn_stickbundle00.nif -Decoding meshes\f\furn_smokestack00.nif -Decoding meshes\f\furn_pathspear_03.nif -Decoding meshes\f\furn_pathspear_02.nif -Decoding meshes\f\furn_pathspear_01.nif -Decoding meshes\f\furn_pathspear_04.nif -Decoding meshes\f\furn_pycave_pool00.nif -Decoding meshes\f\furn_wallscreen_01.nif -Decoding meshes\f\furn_wallscreen_02.nif -Decoding meshes\f\furn_triolith_01a.nif -Decoding meshes\f\furn_halfbarrel01.nif -Decoding meshes\f\furn_halfbarrel00.nif -Decoding meshes\f\furn_imp_altar_01.nif -Decoding meshes\f\furn_imp_metalring.nif -Decoding meshes\f\dwrv_mechlfrarm00.nif -Decoding meshes\f\dwrv_mechrthigh00.nif -Decoding meshes\f\dwrv_mechlthigh00.nif -Decoding meshes\f\furn_bone_stake00.nif -Decoding meshes\f\furn_bone_skull_01.nif -Decoding meshes\f\furn_bannerpost_02.nif -Decoding meshes\f\furn_bannerpost_01.nif -Decoding meshes\f\furn_c_t_shadow_01.nif -Decoding meshes\f\furn_c_t_theif_01.nif -Decoding meshes\f\furn_com_bar_door.nif -Decoding meshes\f\furn_c_t_tower_01.nif -Decoding meshes\f\furn_c_t_arkay_01.nif -Decoding meshes\f\furn_com_kegstand.nif -Decoding meshes\f\furn_clothbolt_02.nif -Decoding meshes\f\furn_clothbolt_03.nif -Decoding meshes\f\furn_clothbolt_01.nif -Decoding meshes\f\furn_com_table_01.nif -Decoding meshes\f\furn_com_table_03.nif -Decoding meshes\f\furn_com_table_02.nif -Decoding meshes\f\furn_com_table_05.nif -Decoding meshes\f\furn_com_table_04.nif -Decoding meshes\f\furn_com_stool_01.nif -Decoding meshes\f\furn_com_stool_02.nif -Decoding meshes\f\furn_com_barstool.nif -Decoding meshes\f\furn_c_t_ritual_01.nif -Decoding meshes\f\furn_c_t_wizard_01.nif -Decoding meshes\f\furn_c_t_lover_01.nif -Decoding meshes\f\furn_crate_lid_01.nif -Decoding meshes\f\furn_com_chair_01.nif -Decoding meshes\f\furn_com_chair_03.nif -Decoding meshes\f\furn_com_chair_02.nif -Decoding meshes\f\furn_com_winerack.nif -Decoding meshes\f\furn_c_t_steed_01.nif -Decoding meshes\f\furn_c_t_golem_01.nif -Decoding meshes\f\furn_com_shelf_04.nif -Decoding meshes\f\furn_com_shelf_03.nif -Decoding meshes\f\furn_com_shelf_02.nif -Decoding meshes\f\furn_com_shelf_01.nif -Decoding meshes\f\furn_crate_open_04.nif -Decoding meshes\f\furn_crate_open_05.nif -Decoding meshes\f\furn_crate_open_01.nif -Decoding meshes\f\furn_com_bench_02.nif -Decoding meshes\f\furn_com_bench_01.nif -Decoding meshes\f\furn_ashl_bugbowl.nif -Decoding meshes\f\furn_ashl_chime_03.nif -Decoding meshes\f\furn_ashl_chime_02.nif -Decoding meshes\f\furn_ashl_chime_01.nif -Decoding meshes\f\furn_ashl_chime_07.nif -Decoding meshes\f\furn_ashl_chime_06.nif -Decoding meshes\f\furn_ashl_chime_05.nif -Decoding meshes\f\furn_ashl_chime_04.nif -Decoding meshes\f\furn_ashl_chime_08.nif -Decoding meshes\f\furn_dwrv_bench10.nif -Decoding meshes\f\furn_dwrv_bench00.nif -Decoding meshes\f\furn_dae_rubble_07.nif -Decoding meshes\f\furn_dae_rubble_06.nif -Decoding meshes\f\furn_dae_rubble_05.nif -Decoding meshes\f\furn_de_ashl_post.nif -Decoding meshes\f\furn_de_shack_post.nif -Decoding meshes\f\furn_dwrv_steam_00.nif -Decoding meshes\f\furn_dwrv_dynamo00.nif -Decoding meshes\f\furn_de_bellows_01.nif -Decoding meshes\f\furn_dwrv_table10.nif -Decoding meshes\f\furn_dwrv_table00.nif -Decoding meshes\f\furn_de_firepit_01.nif -Decoding meshes\f\furn_dwrv_bucket00.nif -Decoding meshes\f\furn_dwrv_table20.nif -Decoding meshes\f\furn_de_shack_hook.nif -Decoding meshes\f\furn_dwrv_chair00.nif -Decoding meshes\f\furn_dwrv_stove00.nif -Decoding meshes\f\furn_dwrv_stove10.nif -Decoding meshes\f\furn_dwrv_tranny01.nif -Decoding meshes\f\furn_dwrv_tranny00.nif -Decoding meshes\f\furn_dwrv_stool00.nif -Decoding meshes\f\furn_dwrv_stool10.nif -Decoding meshes\x\collision01.nif -Decoding meshes\m\key_standard_01.nif -Decoding meshes\m\key_temple_01.nif -Decoding meshes\f\dwrv_mechrarm00.nif -Decoding meshes\f\dwrv_mechhead00.nif -Decoding meshes\f\dwrv_mechlhand00.nif -Decoding meshes\f\dwrv_mechlfoot00.nif -Decoding meshes\f\dwrv_mechrfoot00.nif -Decoding meshes\f\dwrv_mechrfarm00.nif -Decoding meshes\f\dwrv_mechhips00.nif -Decoding meshes\f\dwrv_mechrhand00.nif -Decoding meshes\f\dwrv_mechtorso00.nif -Decoding meshes\f\dwrv_mechlarm00.nif -Decoding meshes\f\furn_c_t_lord_01.nif -Decoding meshes\f\furn_de_winerack.nif -Decoding meshes\f\furn_de_shelf_02.nif -Decoding meshes\f\furn_de_shelf_01.nif -Decoding meshes\f\furn_overhang_09.nif -Decoding meshes\f\furn_overhang_01.nif -Decoding meshes\f\furn_overhang_03.nif -Decoding meshes\f\furn_overhang_02.nif -Decoding meshes\f\furn_overhang_05.nif -Decoding meshes\f\furn_overhang_04.nif -Decoding meshes\f\furn_overhang_07.nif -Decoding meshes\f\furn_overhang_06.nif -Decoding meshes\f\furn_spinwheel00.nif -Decoding meshes\f\furn_overhang_18.nif -Decoding meshes\f\furn_c_t_mara_01.nif -Decoding meshes\f\furn_6th_banner.nif -Decoding meshes\f\furn_de_chair_01.nif -Decoding meshes\f\furn_de_chair_02.nif -Decoding meshes\f\furn_de_chair_03.nif -Decoding meshes\f\furn_de_forge_01.nif -Decoding meshes\f\furn_pottedplant.nif -Decoding meshes\f\furn_triolith_01.nif -Decoding meshes\f\furn_coalpile00.nif -Decoding meshes\f\furn_de_table_07.nif -Decoding meshes\f\furn_de_table_06.nif -Decoding meshes\f\furn_de_table_05.nif -Decoding meshes\f\furn_de_table_04.nif -Decoding meshes\f\furn_de_table_03.nif -Decoding meshes\f\furn_de_table_02.nif -Decoding meshes\f\furn_de_table_01.nif -Decoding meshes\f\furn_de_table_09.nif -Decoding meshes\f\furn_de_table_08.nif -Decoding meshes\f\furn_cistern_01.nif -Decoding meshes\f\furn_com_bar_02.nif -Decoding meshes\f\furn_com_bar_04.nif -Decoding meshes\f\furn_com_bar_06.nif -Decoding meshes\f\furn_tapestry10.nif -Decoding meshes\f\furn_rug_big_08.nif -Decoding meshes\f\furn_rug_big_02.nif -Decoding meshes\f\furn_rug_big_04.nif -Decoding meshes\f\furn_rug_big_06.nif -Decoding meshes\f\furn_cot_rug_02.nif -Decoding meshes\f\furn_de_bench_03.nif -Decoding meshes\f\furn_de_bench_02.nif -Decoding meshes\f\furn_de_bench_01.nif -Decoding meshes\f\furn_de_bench_04.nif -Decoding meshes\f\furn_com_bed_02.nif -Decoding meshes\f\furn_com_bed_04.nif -Decoding meshes\f\furn_com_bed_06.nif -Decoding meshes\f\furn_de_loom_01.nif -Decoding meshes\f\furn_planter_01.nif -Decoding meshes\f\furn_planter_03.nif -Decoding meshes\f\furn_de_rope_05.nif -Decoding meshes\f\furn_de_rope_07.nif -Decoding meshes\f\furn_de_rope_03.nif -Decoding meshes\f\furn_c_t_lady_01.nif -Decoding meshes\f\furn_dwrv_bed00.nif -Decoding meshes\f\furn_rail_end00.nif -Decoding meshes\f\furn_woodbar_01.nif -Decoding meshes\f\furn_de_kegstand.nif -Decoding meshes\f\furn_woodpost_01.nif -Decoding meshes\f\furn_woodpole_01.nif -Decoding meshes\f\furn_tapestry00.nif -Decoding meshes\f\furn_tapestry20.nif -Decoding meshes\f\furn_de_stool_01.nif -Decoding meshes\f\furn_de_stool_02.nif -Decoding meshes\f\furn_de_firepit.nif -Decoding meshes\f\furn_bone_rib_01.nif -Decoding meshes\f\furn_bed_rug_01.nif -Decoding meshes\f\furn_imp_flag_01.nif -Decoding meshes\f\furn_com_bunk_02.nif -Decoding meshes\f\furn_com_bunk_01.nif -Decoding meshes\f\furn_de_lecturn.nif -Decoding meshes\f\furn_guarcart00.nif -Decoding meshes\f\furn_com_planter.nif -Decoding meshes\f\furn_fireplace10.nif -Decoding meshes\f\furn_signbase_02.nif -Decoding meshes\f\furn_com_bar_01.nif -Decoding meshes\f\furn_com_bar_03.nif -Decoding meshes\f\furn_com_bar_05.nif -Decoding meshes\f\furn_tapestry30.nif -Decoding meshes\f\furn_dwrv_well00.nif -Decoding meshes\f\furn_rug_big_09.nif -Decoding meshes\f\furn_rug_big_01.nif -Decoding meshes\f\furn_rug_big_03.nif -Decoding meshes\f\furn_rug_big_05.nif -Decoding meshes\f\furn_rug_big_07.nif -Decoding meshes\f\furn_cot_rug_01.nif -Decoding meshes\f\furn_cot_rug_03.nif -Decoding meshes\f\furn_com_bed_01.nif -Decoding meshes\f\furn_com_bed_03.nif -Decoding meshes\f\furn_com_bed_05.nif -Decoding meshes\f\furn_com_bed_07.nif -Decoding meshes\f\furn_planter_02.nif -Decoding meshes\f\furn_planter_04.nif -Decoding meshes\f\furn_de_rope_04.nif -Decoding meshes\f\furn_de_rope_06.nif -Decoding meshes\f\furn_netramp_01.nif -Decoding meshes\f\furn_rope1_01.nif -Decoding meshes\f\furn_rope2_01.nif -Decoding meshes\f\furn_winekeg00.nif -Decoding meshes\f\furn_bedmat_01.nif -Decoding meshes\f\furn_basin_01.nif -Decoding meshes\f\furn_basket_01.nif -Decoding meshes\f\furn_logpile10.nif -Decoding meshes\f\furn_6th_bell2.nif -Decoding meshes\f\furn_6th_bell4.nif -Decoding meshes\f\furn_6th_bell6.nif -Decoding meshes\f\furn_de_bar_02.nif -Decoding meshes\f\furn_de_bar_04.nif -Decoding meshes\f\furn_de_bar_06.nif -Decoding meshes\f\furn_stool_01.nif -Decoding meshes\f\furn_com_de_01.nif -Decoding meshes\f\furn_ashpit_02.nif -Decoding meshes\f\furn_skull_01.nif -Decoding meshes\f\furn_grill_01.nif -Decoding meshes\f\furn_table_01.nif -Decoding meshes\f\furn_chair_02.nif -Decoding meshes\f\furn_pillow_01.nif -Decoding meshes\f\furn_sconce_01.nif -Decoding meshes\f\furn_banner_01.nif -Decoding meshes\f\furn_cabinet10.nif -Decoding meshes\f\furn_firepit00.nif -Decoding meshes\f\furn_burial10.nif -Decoding meshes\f\furn_burial00.nif -Decoding meshes\f\furn_burial20.nif -Decoding meshes\f\furn_6th_bells.nif -Decoding meshes\f\furn_6th_bell1.nif -Decoding meshes\f\furn_6th_bell3.nif -Decoding meshes\f\furn_6th_bell5.nif -Decoding meshes\f\furn_de_bar_01.nif -Decoding meshes\f\furn_de_bar_03.nif -Decoding meshes\f\furn_de_bar_05.nif -Decoding meshes\f\furn_ashpit_01.nif -Decoding meshes\f\furn_bucket10.nif -Decoding meshes\f\furn_shell10.nif -Decoding meshes\f\furn_shell00.nif -Decoding meshes\f\furn_shell20.nif -Decoding meshes\f\furn_mist512.nif -Decoding meshes\f\furn_rug_04.nif -Decoding meshes\f\furn_rug_01.nif -Decoding meshes\f\furn_rug_03.nif -Decoding meshes\f\furn_rug_02.nif -Decoding meshes\f\furn_torch00.nif -Decoding meshes\f\furn_bone_01.nif -Decoding meshes\f\furn_mist256.nif -Decoding meshes\f\furn_hook_01.nif -Decoding meshes\f\furn_log_04.nif -Decoding meshes\f\furn_log_01.nif -Decoding meshes\f\furn_log_03.nif -Decoding meshes\f\furn_log_02.nif -Decoding meshes\f\furn_table10.nif -Decoding meshes\f\furn_cart00.nif -Decoding meshes\f\furn_anvil00.nif -Decoding meshes\f\furn_well00.nif -Decoding meshes\f\furn_tray_01.nif -Decoding meshes\f\furn_6th_troth_01.nif -Decoding meshes\f\furn_6th_troth_02.nif -Decoding meshes\f\furn_6th_ashaltar.nif -Decoding meshes\f\furn_6th_ashstatue.nif -Decoding meshes\f\furn_6th_ashpillar.nif -Decoding meshes\f\furn_6th_platform.nif -Decoding meshes\w\w_crossbow_steel.nif -Decoding meshes\w\w_corkbulb_arrow.nif -Decoding meshes\w\w_club_daedric.nif -Decoding meshes\w\w_chitin_arrow.nif -Decoding meshes\w\w_chitin_star.nif -Decoding meshes\w\w_chitin_club.nif -Decoding meshes\w\w_chitin_spear.nif -Decoding meshes\w\w_chitin_axe.nif -Decoding meshes\w\w_club_iron.nif -Decoding meshes\a\a_tenpaceboot_gnd.nif -Decoding meshes\a\a_templar_m_f_boot.nif -Decoding meshes\a\a_templar_m_a_boot.nif -Decoding meshes\a\a_templar_m_helmet.nif -Decoding meshes\a\a_templar_w_bracer.nif -Decoding meshes\a\a_templar_m_skins.nif -Decoding meshes\w\w_broadsword_iron.nif -Decoding meshes\w\w_broadsword_ebony.nif -Decoding meshes\m\text_folio_open_04.nif -Decoding meshes\m\text_folio_open_01.nif -Decoding meshes\m\text_folio_open_02.nif -Decoding meshes\m\text_folio_open_03.nif -Decoding meshes\m\text_paper_roll_01.nif -Decoding meshes\m\text_parchment_02.nif -Decoding meshes\m\text_parchment_01.nif -Decoding meshes\xanim_dancinggirl.nif -Decoding meshes\m\text_octavo_05.nif -Decoding meshes\m\text_octavo_07.nif -Decoding meshes\m\text_octavo_01.nif -Decoding meshes\m\text_octavo_03.nif -Decoding meshes\m\text_quarto_03.nif -Decoding meshes\m\text_quarto_01.nif -Decoding meshes\m\text_folio_01.nif -Decoding meshes\m\text_scroll_02.nif -Decoding meshes\m\text_folio_04.nif -Decoding meshes\m\text_folio_03.nif -Decoding meshes\m\text_folio_02.nif -Decoding meshes\m\text_octavo_04.nif -Decoding meshes\m\text_octavo_06.nif -Decoding meshes\m\text_octavo_02.nif -Decoding meshes\m\text_octavo_08.nif -Decoding meshes\m\text_quarto_02.nif -Decoding meshes\m\text_quarto_04.nif -Decoding meshes\m\text_scroll_01.nif -Decoding meshes\m\text_scroll_03.nif -Decoding meshes\m\text_note_02.nif -Decoding meshes\m\text_note_01.nif -Decoding meshes\w\w_bolt_bonemold.nif -Decoding meshes\w\w_bolt_corkbulb.nif -Decoding meshes\w\w_bonemold_arrow.nif -Decoding meshes\w\w_battleaxe_iron.nif -Decoding meshes\a\a_trollbone_cuir.nif -Decoding meshes\a\a_trollbone_helm.nif -Decoding meshes\w\w_bolt_silver.nif -Decoding meshes\w\w_bolt_orcish.nif -Decoding meshes\w\w_bolt_steel.nif -Decoding meshes\w\w_bolt_iron.nif -Decoding meshes\a\a_tenpaceboot.nif -Decoding meshes\w\w_art_queenofbats.nif -Decoding meshes\w\w_art_dagger_fang.nif -Decoding meshes\w\w_art_spear_mercy.nif -Decoding meshes\w\w_art_staff_magnus.nif -Decoding meshes\w\w_art_mace_scourge.nif -Decoding meshes\w\shadowbluntonehand.nif -Decoding meshes\w\shadowblunttwowide.nif -Decoding meshes\w\shadow_towershield.nif -Decoding meshes\w\shadowspeartwowide.nif -Decoding meshes\w\shadowmarksmanbow.nif -Decoding meshes\w\shadowaxetwoclose.nif -Decoding meshes\a\towershield_steel.nif -Decoding meshes\a\towershield_orcish.nif -Decoding meshes\a\towershield_ebony.nif -Decoding meshes\a\towershield_hlaluu.nif -Decoding meshes\a\towershield_glass.nif -Decoding meshes\a\towershield_chitin.nif -Decoding meshes\c\amulet_expensive_1.nif -Decoding meshes\c\amulet_expensive_3.nif -Decoding meshes\c\amulet_expensive_2.nif -Decoding meshes\c\amulet_exquisit_1.nif -Decoding meshes\a\towershield_iron.nif -Decoding meshes\c\amulet_common_4.nif -Decoding meshes\c\amulet_common_2.nif -Decoding meshes\c\amulet_madstone.nif -Decoding meshes\c\amulet_common_5.nif -Decoding meshes\c\amulet_common_1.nif -Decoding meshes\c\amulet_common_3.nif -Decoding meshes\c\amulet_usheeja.nif -Decoding meshes\w\shadowaxeonehand.nif -Decoding meshes\w\shadow_shield.nif -Decoding meshes\w\shadowshield.nif -Decoding meshes\a\a_watchmanshelm.nif -Decoding meshes\w\w_art_azurastar .nif -Decoding meshes\w\w_art_azurastar.nif -Decoding meshes\w\w_art_volendrung.nif -Decoding meshes\w\w_art_keening.nif -Decoding meshes\w\w_art_sunder.nif -Decoding meshes\x\flora_t_podbud_04.nif -Decoding meshes\x\flora_t_podbud_01.nif -Decoding meshes\x\flora_t_podbud_02.nif -Decoding meshes\x\flora_t_podbud_03.nif -Decoding meshes\r\xnetch_betty.nif -Decoding meshes\r\xnetch_bull.nif -Decoding meshes\x\flora_ashtree_03.nif -Decoding meshes\x\flora_ashtree_02.nif -Decoding meshes\x\flora_ashtree_01.nif -Decoding meshes\x\flora_ashtree_07.nif -Decoding meshes\x\flora_ashtree_06.nif -Decoding meshes\x\flora_ashtree_05.nif -Decoding meshes\x\flora_ashtree_04.nif -Decoding meshes\x\flora_ash_log_03.nif -Decoding meshes\x\flora_ash_log_02.nif -Decoding meshes\x\flora_ash_log_01.nif -Decoding meshes\x\flora_ash_log_04.nif -Decoding meshes\r\xleastkagouti.nif -Decoding meshes\e\fire_shield.nif -Decoding meshes\r\xheart_akulakhan.nif -Decoding meshes\xargonian_swimkna.nif -Decoding meshes\r\steam_centurions.nif -Decoding meshes\r\greatbonewalker.nif diff --git a/components/nif/tests/test.sh b/components/nif/tests/test.sh index 2d07708ad..95ecdbfba 100755 --- a/components/nif/tests/test.sh +++ b/components/nif/tests/test.sh @@ -1,18 +1,15 @@ #!/bin/bash -make || exit +#Script to test all nif files (both loose, and in BSA archives) in data files directory -mkdir -p output +DATAFILESDIR="$1" -PROGS=*_test +find "$DATAFILESDIR" -iname *bsa > nifs.txt +find "$DATAFILESDIR" -iname *nif >> nifs.txt -for a in $PROGS; do - if [ -f "output/$a.out" ]; then - echo "Running $a:" - ./$a | diff output/$a.out - - else - echo "Creating $a.out" - ./$a > "output/$a.out" - git add "output/$a.out" - fi -done +sed -e 's/.*/\"&\"/' nifs.txt > quoted_nifs.txt + +xargs --arg-file=quoted_nifs.txt ../../../niftest + +rm nifs.txt +rm quoted_nifs.txt diff --git a/components/nifbullet/bulletnifloader.cpp b/components/nifbullet/bulletnifloader.cpp index cdddb94d0..b366216de 100644 --- a/components/nifbullet/bulletnifloader.cpp +++ b/components/nifbullet/bulletnifloader.cpp @@ -28,6 +28,8 @@ http://www.gnu.org/licenses/ . #include +#include + #include "../nif/niffile.hpp" #include "../nif/node.hpp" #include "../nif/data.hpp" @@ -49,20 +51,6 @@ typedef unsigned char ubyte; namespace NifBullet { -struct TriangleMeshShape : public btBvhTriangleMeshShape -{ - TriangleMeshShape(btStridingMeshInterface* meshInterface, bool useQuantizedAabbCompression) - : btBvhTriangleMeshShape(meshInterface, useQuantizedAabbCompression) - { - } - - virtual ~TriangleMeshShape() - { - delete getTriangleInfoMap(); - delete m_meshInterface; - } -}; - ManualBulletShapeLoader::~ManualBulletShapeLoader() { } @@ -81,15 +69,14 @@ void ManualBulletShapeLoader::loadResource(Ogre::Resource *resource) mBoundingBox = NULL; mShape->mBoxTranslation = Ogre::Vector3(0,0,0); mShape->mBoxRotation = Ogre::Quaternion::IDENTITY; - mHasShape = false; - - btTriangleMesh* mesh1 = new btTriangleMesh(); + mCompoundShape = NULL; + mStaticMesh = NULL; // Load the NIF. TODO: Wrap this in a try-catch block once we're out // of the early stages of development. Right now we WANT to catch // every error as early and intrusively as possible, as it's most // likely a sign of incomplete code rather than faulty input. - Nif::NIFFile::ptr pnif (Nif::NIFFile::create (mResourceName.substr(0, mResourceName.length()-7))); + Nif::NIFFilePtr pnif (Nif::Cache::getInstance().load(mResourceName.substr(0, mResourceName.length()-7))); Nif::NIFFile & nif = *pnif.get (); if (nif.numRoots() < 1) { @@ -111,19 +98,36 @@ void ManualBulletShapeLoader::loadResource(Ogre::Resource *resource) mShape->mHasCollisionNode = hasRootCollisionNode(node); //do a first pass - handleNode(mesh1, node,0,false,false,false); + handleNode(node,0,false,false,false); if(mBoundingBox != NULL) { mShape->mCollisionShape = mBoundingBox; - delete mesh1; + delete mStaticMesh; + if (mCompoundShape) + { + int n = mCompoundShape->getNumChildShapes(); + for(int i=0; i getChildShape(i)); + delete mCompoundShape; + mShape->mAnimatedShapes.clear(); + } } - else if (mHasShape && mShape->mCollide) + else { - mShape->mCollisionShape = new TriangleMeshShape(mesh1,true); + if (mCompoundShape) + { + mShape->mCollisionShape = mCompoundShape; + if (mStaticMesh) + { + btTransform trans; + trans.setIdentity(); + mCompoundShape->addChildShape(trans, new TriangleMeshShape(mStaticMesh,true)); + } + } + else if (mStaticMesh) + mShape->mCollisionShape = new TriangleMeshShape(mStaticMesh,true); } - else - delete mesh1; //second pass which create a shape for raycasting. mResourceName = mShape->getName(); @@ -131,23 +135,23 @@ void ManualBulletShapeLoader::loadResource(Ogre::Resource *resource) mBoundingBox = NULL; mShape->mBoxTranslation = Ogre::Vector3(0,0,0); mShape->mBoxRotation = Ogre::Quaternion::IDENTITY; - mHasShape = false; + mStaticMesh = NULL; + mCompoundShape = NULL; - btTriangleMesh* mesh2 = new btTriangleMesh(); + handleNode(node,0,true,true,false); - handleNode(mesh2, node,0,true,true,false); - - if(mBoundingBox != NULL) + if (mCompoundShape) { - mShape->mRaycastingShape = mBoundingBox; - delete mesh2; - } - else if (mHasShape) - { - mShape->mRaycastingShape = new TriangleMeshShape(mesh2,true); + mShape->mRaycastingShape = mCompoundShape; + if (mStaticMesh) + { + btTransform trans; + trans.setIdentity(); + mCompoundShape->addChildShape(trans, new TriangleMeshShape(mStaticMesh,true)); + } } - else - delete mesh2; + else if (mStaticMesh) + mShape->mRaycastingShape = new TriangleMeshShape(mStaticMesh,true); } bool ManualBulletShapeLoader::hasRootCollisionNode(Nif::Node const * node) @@ -172,14 +176,18 @@ bool ManualBulletShapeLoader::hasRootCollisionNode(Nif::Node const * node) return false; } -void ManualBulletShapeLoader::handleNode(btTriangleMesh* mesh, const Nif::Node *node, int flags, +void ManualBulletShapeLoader::handleNode(const Nif::Node *node, int flags, bool isCollisionNode, - bool raycasting, bool isMarker) + bool raycasting, bool isMarker, bool isAnimated) { // Accumulate the flags from all the child nodes. This works for all // the flags we currently use, at least. flags |= node->flags; + if (!node->controller.empty() && node->controller->recType == Nif::RC_NiKeyframeController + && (node->controller->flags & Nif::NiNode::ControllerFlag_Active)) + isAnimated = true; + if (!raycasting) isCollisionNode = isCollisionNode || (node->recType == Nif::RC_RootCollisionNode); else @@ -227,10 +235,12 @@ void ManualBulletShapeLoader::handleNode(btTriangleMesh* mesh, const Nif::Node * if ( (isCollisionNode || (!mShape->mHasCollisionNode && !raycasting)) && (!isMarker || (mShape->mHasCollisionNode && !raycasting))) { + // NOTE: a trishape with hasBounds=true, but no BBoxCollision flag should NOT go through handleNiTriShape! + // It must be ignored completely. + // (occurs in tr_ex_imp_wall_arch_04.nif) if(node->hasBounds) { - // Checking for BBoxCollision flag causes issues with some actors :/ - if (!(node->flags & Nif::NiNode::Flag_Hidden)) + if (flags & Nif::NiNode::Flag_BBoxCollision && !raycasting) { mShape->mBoxTranslation = node->boundPos; mShape->mBoxRotation = node->boundRot; @@ -240,7 +250,7 @@ void ManualBulletShapeLoader::handleNode(btTriangleMesh* mesh, const Nif::Node * else if(node->recType == Nif::RC_NiTriShape) { mShape->mCollide = !(flags&0x800); - handleNiTriShape(mesh, static_cast(node), flags, node->getWorldTransform(), raycasting); + handleNiTriShape(static_cast(node), flags, node->getWorldTransform(), raycasting, isAnimated); } } @@ -252,13 +262,13 @@ void ManualBulletShapeLoader::handleNode(btTriangleMesh* mesh, const Nif::Node * for(size_t i = 0;i < list.length();i++) { if(!list[i].empty()) - handleNode(mesh, list[i].getPtr(), flags, isCollisionNode, raycasting, isMarker); + handleNode(list[i].getPtr(), flags, isCollisionNode, raycasting, isMarker, isAnimated); } } } -void ManualBulletShapeLoader::handleNiTriShape(btTriangleMesh* mesh, const Nif::NiTriShape *shape, int flags, const Ogre::Matrix4 &transform, - bool raycasting) +void ManualBulletShapeLoader::handleNiTriShape(const Nif::NiTriShape *shape, int flags, const Ogre::Matrix4 &transform, + bool raycasting, bool isAnimated) { assert(shape != NULL); @@ -271,8 +281,6 @@ void ManualBulletShapeLoader::handleNiTriShape(btTriangleMesh* mesh, const Nif:: // anything. So don't do anything. if ((flags & 0x800) && !raycasting) { - collide = false; - bbcollide = false; return; } @@ -281,17 +289,64 @@ void ManualBulletShapeLoader::handleNiTriShape(btTriangleMesh* mesh, const Nif:: // bother setting it up. return; - mHasShape = true; + if (!shape->skin.empty()) + isAnimated = false; - const Nif::NiTriShapeData *data = shape->data.getPtr(); - const std::vector &vertices = data->vertices; - const short *triangles = &data->triangles[0]; - for(size_t i = 0;i < data->triangles.size();i+=3) + if (isAnimated) { - Ogre::Vector3 b1 = transform*vertices[triangles[i+0]]; - Ogre::Vector3 b2 = transform*vertices[triangles[i+1]]; - Ogre::Vector3 b3 = transform*vertices[triangles[i+2]]; - mesh->addTriangle(btVector3(b1.x,b1.y,b1.z),btVector3(b2.x,b2.y,b2.z),btVector3(b3.x,b3.y,b3.z)); + if (!mCompoundShape) + mCompoundShape = new btCompoundShape(); + + btTriangleMesh* childMesh = new btTriangleMesh(); + + const Nif::NiTriShapeData *data = shape->data.getPtr(); + + childMesh->preallocateVertices(data->vertices.size()); + childMesh->preallocateIndices(data->triangles.size()); + + const std::vector &vertices = data->vertices; + const std::vector &triangles = data->triangles; + + for(size_t i = 0;i < data->triangles.size();i+=3) + { + Ogre::Vector3 b1 = vertices[triangles[i+0]]; + Ogre::Vector3 b2 = vertices[triangles[i+1]]; + Ogre::Vector3 b3 = vertices[triangles[i+2]]; + childMesh->addTriangle(btVector3(b1.x,b1.y,b1.z),btVector3(b2.x,b2.y,b2.z),btVector3(b3.x,b3.y,b3.z)); + } + + TriangleMeshShape* childShape = new TriangleMeshShape(childMesh,true); + + childShape->setLocalScaling(btVector3(transform[0][0], transform[1][1], transform[2][2])); + + Ogre::Quaternion q = transform.extractQuaternion(); + Ogre::Vector3 v = transform.getTrans(); + btTransform trans(btQuaternion(q.x, q.y, q.z, q.w), btVector3(v.x, v.y, v.z)); + + if (raycasting) + mShape->mAnimatedRaycastingShapes.insert(std::make_pair(shape->name, mCompoundShape->getNumChildShapes())); + else + mShape->mAnimatedShapes.insert(std::make_pair(shape->name, mCompoundShape->getNumChildShapes())); + + mCompoundShape->addChildShape(trans, childShape); + } + else + { + if (!mStaticMesh) + mStaticMesh = new btTriangleMesh(); + + // Static shape, just transform all vertices into position + const Nif::NiTriShapeData *data = shape->data.getPtr(); + const std::vector &vertices = data->vertices; + const std::vector &triangles = data->triangles; + + for(size_t i = 0;i < data->triangles.size();i+=3) + { + Ogre::Vector3 b1 = transform*vertices[triangles[i+0]]; + Ogre::Vector3 b2 = transform*vertices[triangles[i+1]]; + Ogre::Vector3 b3 = transform*vertices[triangles[i+2]]; + mStaticMesh->addTriangle(btVector3(b1.x,b1.y,b1.z),btVector3(b2.x,b2.y,b2.z),btVector3(b3.x,b3.y,b3.z)); + } } } @@ -304,4 +359,53 @@ void ManualBulletShapeLoader::load(const std::string &name,const std::string &gr OEngine::Physic::BulletShapeManager::getSingleton().create(name,group,true,this); } +bool findBoundingBox (const Nif::Node* node, Ogre::Vector3& halfExtents, Ogre::Vector3& translation, Ogre::Quaternion& orientation) +{ + if(node->hasBounds) + { + if (!(node->flags & Nif::NiNode::Flag_Hidden)) + { + translation = node->boundPos; + orientation = node->boundRot; + halfExtents = node->boundXYZ; + return true; + } + } + + const Nif::NiNode *ninode = dynamic_cast(node); + if(ninode) + { + const Nif::NodeList &list = ninode->children; + for(size_t i = 0;i < list.length();i++) + { + if(!list[i].empty()) + if (findBoundingBox(list[i].getPtr(), halfExtents, translation, orientation)) + return true; + } + } + return false; +} + +bool getBoundingBox(const std::string& nifFile, Ogre::Vector3& halfExtents, Ogre::Vector3& translation, Ogre::Quaternion& orientation) +{ + Nif::NIFFilePtr pnif (Nif::Cache::getInstance().load(nifFile)); + Nif::NIFFile & nif = *pnif.get (); + + if (nif.numRoots() < 1) + { + return false; + } + + Nif::Record *r = nif.getRoot(0); + assert(r != NULL); + + Nif::Node *node = dynamic_cast(r); + if (node == NULL) + { + return false; + } + + return findBoundingBox(node, halfExtents, translation, orientation); +} + } // namespace NifBullet diff --git a/components/nifbullet/bulletnifloader.hpp b/components/nifbullet/bulletnifloader.hpp index d1e876305..a9ee968b9 100644 --- a/components/nifbullet/bulletnifloader.hpp +++ b/components/nifbullet/bulletnifloader.hpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include @@ -44,6 +45,22 @@ namespace Nif namespace NifBullet { +// Subclass btBhvTriangleMeshShape to auto-delete the meshInterface +struct TriangleMeshShape : public btBvhTriangleMeshShape +{ + TriangleMeshShape(btStridingMeshInterface* meshInterface, bool useQuantizedAabbCompression) + : btBvhTriangleMeshShape(meshInterface, useQuantizedAabbCompression) + { + } + + virtual ~TriangleMeshShape() + { + delete getTriangleInfoMap(); + delete m_meshInterface; + } +}; + + /** *Load bulletShape from NIF files. */ @@ -52,8 +69,9 @@ class ManualBulletShapeLoader : public OEngine::Physic::BulletShapeLoader public: ManualBulletShapeLoader() : mShape(NULL) + , mStaticMesh(NULL) + , mCompoundShape(NULL) , mBoundingBox(NULL) - , mHasShape(false) { } @@ -88,7 +106,8 @@ private: /** *Parse a node. */ - void handleNode(btTriangleMesh* mesh, Nif::Node const *node, int flags, bool isCollisionNode, bool raycasting, bool isMarker); + void handleNode(Nif::Node const *node, int flags, bool isCollisionNode, + bool raycasting, bool isMarker, bool isAnimated=false); /** *Helper function @@ -98,16 +117,24 @@ private: /** *convert a NiTriShape to a bullet trishape. */ - void handleNiTriShape(btTriangleMesh* mesh, const Nif::NiTriShape *shape, int flags, const Ogre::Matrix4 &transform, bool raycasting); + void handleNiTriShape(const Nif::NiTriShape *shape, int flags, const Ogre::Matrix4 &transform, bool raycasting, bool isAnimated); std::string mResourceName; OEngine::Physic::BulletShape* mShape;//current shape - btBoxShape *mBoundingBox; - bool mHasShape; + btCompoundShape* mCompoundShape; + + btTriangleMesh* mStaticMesh; + + btBoxShape *mBoundingBox; }; + +bool getBoundingBox(const std::string& nifFile, Ogre::Vector3& halfExtents, Ogre::Vector3& translation, Ogre::Quaternion& orientation); + +bool findBoundingBox(const Nif::Node* node, Ogre::Vector3& halfExtents, Ogre::Vector3& translation, Ogre::Quaternion& orientation); + } #endif diff --git a/components/nifbullet/test/test.cpp b/components/nifbullet/test/test.cpp deleted file mode 100644 index 261edf512..000000000 --- a/components/nifbullet/test/test.cpp +++ /dev/null @@ -1,209 +0,0 @@ -#include "bullet_nif_loader.hpp" -#include "..\nifogre\ogre_nif_loader.hpp" -#include "..\bsa\bsa_archive.hpp" -#include "..\nifogre\ogre_nif_loader.hpp" -#include -#include -#include -#include -#include "BtOgrePG.h" -#include "BtOgreGP.h" -#include "BtOgreExtras.h" - -const char* mesh = "meshes\\x\\ex_hlaalu_b_24.nif"; - -class MyMotionState : public btMotionState { -public: - MyMotionState(const btTransform &initialpos, Ogre::SceneNode *node) { - mVisibleobj = node; - mPos1 = initialpos; - node->setPosition(initialpos.getOrigin().x(),initialpos.getOrigin().y(),initialpos.getOrigin().z()); - } - - virtual ~MyMotionState() { - } - - void setNode(Ogre::SceneNode *node) { - mVisibleobj = node; - } - - virtual void getWorldTransform(btTransform &worldTrans) const { - worldTrans = mPos1; - } - - virtual void setWorldTransform(const btTransform &worldTrans) { - if(NULL == mVisibleobj) return; // silently return before we set a node - btQuaternion rot = worldTrans.getRotation(); - mVisibleobj->setOrientation(rot.w(), rot.x(), rot.y(), rot.z()); - btVector3 pos = worldTrans.getOrigin(); - mVisibleobj->setPosition(pos.x(), pos.y(), pos.z()); - } - -protected: - Ogre::SceneNode *mVisibleobj; - btTransform mPos1; -}; - - -int main() -{ - try - { - //Ogre stuff - - Ogre::Root* pRoot = new Ogre::Root(); - pRoot->showConfigDialog(); - - BulletShapeManager* manag = new BulletShapeManager(); - - Ogre::RenderWindow* win = pRoot->initialise(true,"test"); - Ogre::SceneManager* scmg = pRoot->createSceneManager(Ogre::ST_GENERIC,"MonGestionnaireDeScene"); - Ogre::Camera* pCamera = scmg->createCamera("test"); - Ogre::Viewport* pViewport = win->addViewport(pCamera); - pCamera->setPosition(-50,0,0); - pCamera->setFarClipDistance(10000); - pCamera->setNearClipDistance(1.); - pCamera->lookAt(0,0,0); - //Ogre::ResourceGroupManager::getSingleton().addResourceLocation("C++/OgreSK/media/models","FileSystem","General"); - Ogre::ResourceGroupManager::getSingleton().addResourceLocation("","FileSystem","General"); - /*Ogre::ResourceGroupManager::getSingleton().addResourceLocation("C++/OgreSK/media/materials/scripts","FileSystem","General"); - Ogre::ResourceGroupManager::getSingleton().addResourceLocation("C++/OgreSK/media/materials/textures","FileSystem","General"); - Ogre::ResourceGroupManager::getSingleton().addResourceLocation("C++/OgreSK/media/materials/programs","FileSystem","General");*/ - - - //OIS stuff - OIS::ParamList pl; - size_t windowHnd = 0; - std::ostringstream windowHndStr; - win->getCustomAttribute("WINDOW", &windowHnd); - windowHndStr << windowHnd; - pl.insert(std::make_pair(std::string("WINDOW"), windowHndStr.str())); - OIS::InputManager *pInputManager = OIS::InputManager::createInputSystem( pl ); - OIS::Mouse *pMouse = static_cast(pInputManager->createInputObject(OIS::OISMouse, false)); - OIS::Keyboard* pKeyboard = static_cast(pInputManager->createInputObject(OIS::OISKeyboard, false)); - unsigned int width, height, depth; - int top, left; - win->getMetrics(width, height, depth, left, top); - const OIS::MouseState &ms = pMouse->getMouseState(); - ms.width = width; - ms.height = height; - - - //Ressources stuff - Bsa::addBSA("Morrowind.bsa"); - //Ogre::ResourceGroupManager::getSingleton().createResourceGroup("general"); - - Ogre::ResourcePtr ptr = BulletShapeManager::getSingleton().getByName(mesh,"General"); - NifBullet::ManualBulletShapeLoader* ShapeLoader = new NifBullet::ManualBulletShapeLoader(); - - ShapeLoader->load(mesh,"General"); - //BulletShapeManager::getSingleton().unload(mesh); - //ShapeLoader->load(mesh,"General"); - - NIFLoader::load(mesh); - - Ogre::ResourceGroupManager::getSingleton().initialiseAllResourceGroups(); - //BulletShapeManager::getSingleton(). - BulletShapePtr shape = BulletShapeManager::getSingleton().getByName(mesh,"General"); - BulletShapeManager::getSingleton().load(mesh,"General"); - BulletShapeManager::getSingleton().unload(mesh); - BulletShapeManager::getSingleton().load(mesh,"General"); - BulletShapeManager::getSingleton().load(mesh,"General"); - //shape->load(); - //shape->unload(); - //shape->load(); - - //Bullet init - btBroadphaseInterface* broadphase = new btDbvtBroadphase(); - - // Set up the collision configuration and dispatcher - btDefaultCollisionConfiguration* collisionConfiguration = new btDefaultCollisionConfiguration(); - btCollisionDispatcher* dispatcher = new btCollisionDispatcher(collisionConfiguration); - - // The actual physics solver - btSequentialImpulseConstraintSolver* solver = new btSequentialImpulseConstraintSolver; - - // The world. - btDiscreteDynamicsWorld* dynamicsWorld = new btDiscreteDynamicsWorld(dispatcher,broadphase,solver,collisionConfiguration); - dynamicsWorld->setGravity(btVector3(0,-10,0)); - - - - //le sol? - Ogre::SceneNode *node = scmg->getRootSceneNode()->createChildSceneNode("node"); - Ogre::Entity *ent = scmg->createEntity("Mesh1",mesh); - node->attachObject(ent); - MyMotionState* mst = new MyMotionState(btTransform::getIdentity(),node); - - btRigidBody::btRigidBodyConstructionInfo groundRigidBodyCI(0,mst,shape->Shape,btVector3(0,0,0)); - btRigidBody* groundRigidBody = new btRigidBody(groundRigidBodyCI); - dynamicsWorld->addRigidBody(groundRigidBody); - - //une balle: - Ogre::SceneNode *node2 = scmg->getRootSceneNode()->createChildSceneNode("node2"); - Ogre::Entity *ent2 = scmg->createEntity("Mesh2","ogrehead.mesh"); - node2->attachObject(ent2); - node2->setPosition(0,500,0); - btTransform iT; - iT.setIdentity(); - iT.setOrigin(btVector3(0,5000,0)); - MyMotionState* mst2 = new MyMotionState(btTransform::getIdentity(),node2); - - btSphereShape* sphereshape = new btSphereShape(10); - btRigidBody::btRigidBodyConstructionInfo sphereCI(10,mst2,sphereshape,btVector3(0,0,0)); - btRigidBody* sphere = new btRigidBody(sphereCI); - dynamicsWorld->addRigidBody(sphere); - - - //btOgre! - BtOgre::DebugDrawer* mDebugDrawer = new BtOgre::DebugDrawer(scmg->getRootSceneNode(), dynamicsWorld); - dynamicsWorld->setDebugDrawer(mDebugDrawer); - - Ogre::Timer timer; - timer.reset(); - bool cont = true; - while(cont) - { - if(timer.getMilliseconds()>30) - { - pMouse->capture(); - pKeyboard->capture(); - - Ogre::Vector3 a(0,0,0); - - if(pKeyboard->isKeyDown(OIS::KC_UP)) - { - a = a + Ogre::Vector3(0,0,-20); - } - if(pKeyboard->isKeyDown(OIS::KC_DOWN)) - { - a = a + Ogre::Vector3(0,0,20); - } - if(pKeyboard->isKeyDown(OIS::KC_ESCAPE)) - { - cont = false; - } - OIS::MouseState MS = pMouse->getMouseState(); - pCamera->yaw(-Ogre::Degree(MS.X.rel)); - pCamera->pitch(-Ogre::Degree(MS.Y.rel)); - pCamera->moveRelative(a); - - pRoot->renderOneFrame(); - mDebugDrawer->step(); - timer.reset(); - dynamicsWorld->stepSimulation(0.03); - } - } - std::cout << "cool"; - delete manag; - delete pRoot; - char a; - std::cin >> a; - } - catch(Ogre::Exception& e) - { - std::cout << e.getFullDescription(); - char a; - std::cin >> a; - } -} diff --git a/components/nifcache/nifcache.cpp b/components/nifcache/nifcache.cpp new file mode 100644 index 000000000..342251dbc --- /dev/null +++ b/components/nifcache/nifcache.cpp @@ -0,0 +1,40 @@ +#include "nifcache.hpp" + +namespace Nif +{ + +Cache* Cache::sThis = 0; + +Cache& Cache::getInstance() +{ + assert (sThis); + return *sThis; +} + +Cache* Cache::getInstancePtr() +{ + return sThis; +} + +Cache::Cache() +{ + assert (!sThis); + sThis = this; +} + +NIFFilePtr Cache::load(const std::string &filename) +{ + // TODO: normalize file path to make sure we're not loading the same file twice + + LoadedMap::iterator it = mLoadedMap.find(filename); + if (it != mLoadedMap.end()) + return it->second; + else + { + NIFFilePtr file(new Nif::NIFFile(filename)); + mLoadedMap[filename] = file; + return file; + } +} + +} diff --git a/components/nifcache/nifcache.hpp b/components/nifcache/nifcache.hpp new file mode 100644 index 000000000..173b91865 --- /dev/null +++ b/components/nifcache/nifcache.hpp @@ -0,0 +1,50 @@ +#ifndef OPENMW_COMPONENTS_NIFCACHE_H +#define OPENMW_COMPONENTS_NIFCACHE_H + +#include + +#include + +#include + +namespace Nif +{ + + typedef boost::shared_ptr NIFFilePtr; + + /// @brief A basic resource manager for NIF files + class Cache + { + public: + Cache(); + + /// Queue this file for background loading. A worker thread will start loading the file. + /// To get the loaded NIFFilePtr, use the load method, which will wait until the worker thread is finished + /// and then return the loaded file. + //void loadInBackground (const std::string& file); + + /// Read and parse the given file. May retrieve from cache if this file has been used previously. + /// @note If the file is currently loading in the background, this function will block until + /// the background loading finishes, then return the background loaded file. + /// @note Returns a SharedPtr to the file and the file will stay loaded as long as the user holds on to this pointer. + /// When all external SharedPtrs to a file are released, the cache may decide to unload the file. + NIFFilePtr load (const std::string& filename); + + /// Return instance of this class. + static Cache& getInstance(); + static Cache* getInstancePtr(); + + private: + static Cache* sThis; + + Cache(const Cache&); + Cache& operator =(const Cache&); + + typedef std::map LoadedMap; + + LoadedMap mLoadedMap; + }; + +} + +#endif diff --git a/components/nifogre/controller.hpp b/components/nifogre/controller.hpp index 317447d95..cc750ea65 100644 --- a/components/nifogre/controller.hpp +++ b/components/nifogre/controller.hpp @@ -2,6 +2,7 @@ #define COMPONENTS_NIFOGRE_CONTROLLER_H #include +#include #include namespace NifOgre @@ -10,53 +11,55 @@ namespace NifOgre class ValueInterpolator { protected: - float interpKey(const Nif::FloatKeyList::VecType &keys, float time, float def=0.f) const + float interpKey(const Nif::FloatKeyMap::MapType &keys, float time, float def=0.f) const { if (keys.size() == 0) return def; - if(time <= keys.front().mTime) - return keys.front().mValue; + if(time <= keys.begin()->first) + return keys.begin()->second.mValue; - const Nif::FloatKey* keyArray = keys.data(); - size_t size = keys.size(); - - for (size_t i = 1; i < size; ++i) + Nif::FloatKeyMap::MapType::const_iterator it = keys.lower_bound(time); + if (it != keys.end()) { - const Nif::FloatKey* aKey = &keyArray[i]; + float aTime = it->first; + const Nif::FloatKey* aKey = &it->second; + + assert (it != keys.begin()); // Shouldn't happen, was checked at beginning of this function - if(aKey->mTime < time) - continue; + Nif::FloatKeyMap::MapType::const_iterator last = --it; + float aLastTime = last->first; + const Nif::FloatKey* aLastKey = &last->second; - const Nif::FloatKey* aLastKey = &keyArray[i-1]; - float a = (time - aLastKey->mTime) / (aKey->mTime - aLastKey->mTime); + float a = (time - aLastTime) / (aTime - aLastTime); return aLastKey->mValue + ((aKey->mValue - aLastKey->mValue) * a); } - - return keys.back().mValue; + else + return keys.rbegin()->second.mValue; } - Ogre::Vector3 interpKey(const Nif::Vector3KeyList::VecType &keys, float time) const + Ogre::Vector3 interpKey(const Nif::Vector3KeyMap::MapType &keys, float time) const { - if(time <= keys.front().mTime) - return keys.front().mValue; - - const Nif::Vector3Key* keyArray = keys.data(); - size_t size = keys.size(); + if(time <= keys.begin()->first) + return keys.begin()->second.mValue; - for (size_t i = 1; i < size; ++i) + Nif::Vector3KeyMap::MapType::const_iterator it = keys.lower_bound(time); + if (it != keys.end()) { - const Nif::Vector3Key* aKey = &keyArray[i]; + float aTime = it->first; + const Nif::Vector3Key* aKey = &it->second; - if(aKey->mTime < time) - continue; + assert (it != keys.begin()); // Shouldn't happen, was checked at beginning of this function - const Nif::Vector3Key* aLastKey = &keyArray[i-1]; - float a = (time - aLastKey->mTime) / (aKey->mTime - aLastKey->mTime); + Nif::Vector3KeyMap::MapType::const_iterator last = --it; + float aLastTime = last->first; + const Nif::Vector3Key* aLastKey = &last->second; + + float a = (time - aLastTime) / (aTime - aLastTime); return aLastKey->mValue + ((aKey->mValue - aLastKey->mValue) * a); } - - return keys.back().mValue; + else + return keys.rbegin()->second.mValue; } }; @@ -86,6 +89,9 @@ namespace NifOgre { if(mDeltaInput) { + if (mStopTime - mStartTime == 0.f) + return 0.f; + mDeltaCount += value*mFrequency; if(mDeltaCount < mStartTime) mDeltaCount = mStopTime - std::fmod(mStartTime - mDeltaCount, diff --git a/components/nifogre/material.cpp b/components/nifogre/material.cpp index 3a87e1d52..517f29f4e 100644 --- a/components/nifogre/material.cpp +++ b/components/nifogre/material.cpp @@ -1,7 +1,7 @@ #include "material.hpp" #include -#include +#include #include #include @@ -54,49 +54,6 @@ static const char *getTestMode(int mode) return "less_equal"; } - -std::string NIFMaterialLoader::findTextureName(const std::string &filename) -{ - /* Bethesda at some point converted all their BSA - * textures from tga to dds for increased load speed, but all - * texture file name references were kept as .tga. - */ - static const char path[] = "textures\\"; - static const char path2[] = "textures/"; - - std::string texname = filename; - Misc::StringUtils::toLower(texname); - - // Apparently, leading separators are allowed - while (texname.size() && (texname[0] == '/' || texname[0] == '\\')) - texname.erase(0, 1); - - if(texname.compare(0, sizeof(path)-1, path) != 0 && - texname.compare(0, sizeof(path2)-1, path2) != 0) - texname = path + texname; - - Ogre::String::size_type pos = texname.rfind('.'); - if(pos != Ogre::String::npos && texname.compare(pos, texname.length() - pos, ".dds") != 0) - { - // since we know all (GOTY edition or less) textures end - // in .dds, we change the extension - texname.replace(pos, texname.length(), ".dds"); - - // if it turns out that the above wasn't true in all cases (not for vanilla, but maybe mods) - // verify, and revert if false (this call succeeds quickly, but fails slowly) - if(!Ogre::ResourceGroupManager::getSingleton().resourceExistsInAnyGroup(texname)) - { - texname = filename; - Misc::StringUtils::toLower(texname); - if(texname.compare(0, sizeof(path)-1, path) != 0 && - texname.compare(0, sizeof(path2)-1, path2) != 0) - texname = path + texname; - } - } - - return texname; -} - Ogre::String NIFMaterialLoader::getMaterial(const Nif::ShapeData *shapedata, const Ogre::String &name, const Ogre::String &group, const Nif::NiTexturingProperty *texprop, @@ -106,7 +63,7 @@ Ogre::String NIFMaterialLoader::getMaterial(const Nif::ShapeData *shapedata, const Nif::NiZBufferProperty *zprop, const Nif::NiSpecularProperty *specprop, const Nif::NiWireframeProperty *wireprop, - bool &needTangents) + bool &needTangents, bool particleMaterial) { Ogre::MaterialManager &matMgr = Ogre::MaterialManager::getSingleton(); Ogre::MaterialPtr material = matMgr.getByName(name); @@ -146,7 +103,7 @@ Ogre::String NIFMaterialLoader::getMaterial(const Nif::ShapeData *shapedata, const Nif::NiSourceTexture *st = texprop->textures[i].texture.getPtr(); if(st->external) - texName[i] = findTextureName(st->filename); + texName[i] = Misc::ResourceHelpers::correctTexturePath(st->filename); else warn("Found internal texture, ignoring."); } @@ -245,6 +202,11 @@ Ogre::String NIFMaterialLoader::getMaterial(const Nif::ShapeData *shapedata, } } + if (particleMaterial) + { + alpha = 1.f; // Apparently ignored, might be overridden by particle vertex colors? + } + { // Generate a hash out of all properties that can affect the material. size_t h = 0; @@ -393,11 +355,17 @@ Ogre::String NIFMaterialLoader::getMaterial(const Nif::ShapeData *shapedata, if((alphaFlags>>9)&1) { +#ifndef ANDROID std::string reject; reject += getTestMode((alphaFlags>>10)&0x7); reject += " "; reject += Ogre::StringConverter::toString(alphaTest); instance->setProperty("alpha_rejection", sh::makeProperty(new sh::StringValue(reject))); +#else + // alpha test not supported in OpenGL ES 2, use manual implementation in shader + instance->setProperty("alphaTestMode", sh::makeProperty(new sh::IntValue((alphaFlags>>10)&0x7))); + instance->setProperty("alphaTestValue", sh::makeProperty(new sh::FloatValue(alphaTest/255.f))); +#endif } else instance->getMaterial()->setShadowCasterMaterial("openmw_shadowcaster_noalpha"); diff --git a/components/nifogre/material.hpp b/components/nifogre/material.hpp index b02c7c236..d485439cf 100644 --- a/components/nifogre/material.hpp +++ b/components/nifogre/material.hpp @@ -29,17 +29,9 @@ class NIFMaterialLoader { std::cerr << "NIFMaterialLoader: Warn: " << msg << std::endl; } - static void fail(const std::string &msg) - { - std::cerr << "NIFMaterialLoader: Fail: "<< msg << std::endl; - abort(); - } - static std::map sMaterialMap; public: - static std::string findTextureName(const std::string &filename); - static Ogre::String getMaterial(const Nif::ShapeData *shapedata, const Ogre::String &name, const Ogre::String &group, const Nif::NiTexturingProperty *texprop, @@ -49,7 +41,7 @@ public: const Nif::NiZBufferProperty *zprop, const Nif::NiSpecularProperty *specprop, const Nif::NiWireframeProperty *wireprop, - bool &needTangents); + bool &needTangents, bool particleMaterial=false); }; } diff --git a/components/nifogre/mesh.cpp b/components/nifogre/mesh.cpp index 8bebe0589..c952e664d 100644 --- a/components/nifogre/mesh.cpp +++ b/components/nifogre/mesh.cpp @@ -15,6 +15,7 @@ #include #include +#include #include #include "material.hpp" @@ -137,9 +138,10 @@ void NIFMeshLoader::createSubMesh(Ogre::Mesh *mesh, const Nif::NiTriShape *shape const Nif::NodeList &bones = skin->bones; for(size_t b = 0;b < bones.length();b++) { - Ogre::Matrix4 mat; - mat.makeTransform(data->bones[b].trafo.trans, Ogre::Vector3(data->bones[b].trafo.scale), - Ogre::Quaternion(data->bones[b].trafo.rotation)); + const Ogre::Matrix3& rotationScale = data->bones[b].trafo.rotationScale; + Ogre::Matrix4 mat (rotationScale); + mat.setTrans(data->bones[b].trafo.trans); + mat.setScale(Ogre::Vector3(rotationScale[0][0], rotationScale[1][1], rotationScale[2][2]) * data->bones[b].trafo.scale); mat = bones[b]->getWorldTransform() * mat; const std::vector &weights = data->bones[b].weights; @@ -383,7 +385,7 @@ void NIFMeshLoader::loadResource(Ogre::Resource *resource) Ogre::Mesh *mesh = dynamic_cast(resource); OgreAssert(mesh, "Attempting to load a mesh into a non-mesh resource!"); - Nif::NIFFile::ptr nif = Nif::NIFFile::create(mName); + Nif::NIFFilePtr nif = Nif::Cache::getInstance().load(mName); if(mShapeIndex >= nif->numRecords()) { Ogre::SkeletonManager *skelMgr = Ogre::SkeletonManager::getSingletonPtr(); diff --git a/components/nifogre/ogrenifloader.cpp b/components/nifogre/ogrenifloader.cpp index 36d750821..dcc34f627 100644 --- a/components/nifogre/ogrenifloader.cpp +++ b/components/nifogre/ogrenifloader.cpp @@ -45,12 +45,32 @@ #include #include +#include #include +#include #include "skeleton.hpp" #include "material.hpp" #include "mesh.hpp" #include "controller.hpp" +#include "particles.hpp" + +namespace +{ + + void getAllNiNodes(const Nif::Node* node, std::vector& out) + { + const Nif::NiNode* ninode = dynamic_cast(node); + if (ninode) + { + out.push_back(ninode); + for (unsigned int i=0; ichildren.length(); ++i) + if (!ninode->children[i].empty()) + getAllNiNodes(ninode->children[i].getPtr(), out); + } + } + +} namespace NifOgre { @@ -115,6 +135,21 @@ ObjectScene::~ObjectScene() mSkelBase = NULL; } +void ObjectScene::setVisibilityFlags (unsigned int flags) +{ + for (std::vector::iterator iter (mEntities.begin()); iter!=mEntities.end(); + ++iter) + (*iter)->setVisibilityFlags (flags); + + for (std::vector::iterator iter (mParticles.begin()); + iter!=mParticles.end(); ++iter) + (*iter)->setVisibilityFlags (flags); + + for (std::vector::iterator iter (mLights.begin()); iter!=mLights.end(); + ++iter) + (*iter)->setVisibilityFlags (flags); +} + void ObjectScene::rotateBillboardNodes(Ogre::Camera *camera) { for (std::vector::iterator it = mBillboardNodes.begin(); it != mBillboardNodes.end(); ++it) @@ -151,7 +186,7 @@ public: const Nif::NiSourceTexture* tex = ctrl->mSources[i].getPtr(); if (!tex->external) std::cerr << "Warning: Found internal texture, ignoring." << std::endl; - mTextures.push_back(NIFMaterialLoader::findTextureName(tex->filename)); + mTextures.push_back(Misc::ResourceHelpers::correctTexturePath(tex->filename)); } } @@ -202,7 +237,7 @@ public: { private: Ogre::MovableObject* mMovable; - Nif::FloatKeyList mData; + Nif::FloatKeyMap mData; MaterialControllerManager* mMaterialControllerMgr; public: @@ -249,7 +284,7 @@ public: { private: Ogre::MovableObject* mMovable; - Nif::Vector3KeyList mData; + Nif::Vector3KeyMap mData; MaterialControllerManager* mMaterialControllerMgr; public: @@ -374,61 +409,65 @@ public: class Value : public NodeTargetValue, public ValueInterpolator { private: - Nif::QuaternionKeyList mRotations; - Nif::Vector3KeyList mTranslations; - Nif::FloatKeyList mScales; + const Nif::QuaternionKeyMap* mRotations; + const Nif::Vector3KeyMap* mTranslations; + const Nif::FloatKeyMap* mScales; + Nif::NIFFilePtr mNif; // Hold a SharedPtr to make sure key lists stay valid using ValueInterpolator::interpKey; - static Ogre::Quaternion interpKey(const Nif::QuaternionKeyList::VecType &keys, float time) + static Ogre::Quaternion interpKey(const Nif::QuaternionKeyMap::MapType &keys, float time) { - if(time <= keys.front().mTime) - return keys.front().mValue; - - const Nif::QuaternionKey* keyArray = keys.data(); - size_t size = keys.size(); + if(time <= keys.begin()->first) + return keys.begin()->second.mValue; - for (size_t i = 1; i < size; ++i) + Nif::QuaternionKeyMap::MapType::const_iterator it = keys.lower_bound(time); + if (it != keys.end()) { - const Nif::QuaternionKey* aKey = &keyArray[i]; + float aTime = it->first; + const Nif::QuaternionKey* aKey = &it->second; - if(aKey->mTime < time) - continue; + assert (it != keys.begin()); // Shouldn't happen, was checked at beginning of this function + + Nif::QuaternionKeyMap::MapType::const_iterator last = --it; + float aLastTime = last->first; + const Nif::QuaternionKey* aLastKey = &last->second; - const Nif::QuaternionKey* aLastKey = &keyArray[i-1]; - float a = (time - aLastKey->mTime) / (aKey->mTime - aLastKey->mTime); + float a = (time - aLastTime) / (aTime - aLastTime); return Ogre::Quaternion::nlerp(a, aLastKey->mValue, aKey->mValue); } - - return keys.back().mValue; + else + return keys.rbegin()->second.mValue; } public: - Value(Ogre::Node *target, const Nif::NiKeyframeData *data) + /// @note The NiKeyFrameData must be valid as long as this KeyframeController exists. + Value(Ogre::Node *target, const Nif::NIFFilePtr& nif, const Nif::NiKeyframeData *data) : NodeTargetValue(target) - , mRotations(data->mRotations) - , mTranslations(data->mTranslations) - , mScales(data->mScales) + , mRotations(&data->mRotations) + , mTranslations(&data->mTranslations) + , mScales(&data->mScales) + , mNif(nif) { } virtual Ogre::Quaternion getRotation(float time) const { - if(mRotations.mKeys.size() > 0) - return interpKey(mRotations.mKeys, time); + if(mRotations->mKeys.size() > 0) + return interpKey(mRotations->mKeys, time); return mNode->getOrientation(); } virtual Ogre::Vector3 getTranslation(float time) const { - if(mTranslations.mKeys.size() > 0) - return interpKey(mTranslations.mKeys, time); + if(mTranslations->mKeys.size() > 0) + return interpKey(mTranslations->mKeys, time); return mNode->getPosition(); } virtual Ogre::Vector3 getScale(float time) const { - if(mScales.mKeys.size() > 0) - return Ogre::Vector3(interpKey(mScales.mKeys, time)); + if(mScales->mKeys.size() > 0) + return Ogre::Vector3(interpKey(mScales->mKeys, time)); return mNode->getScale(); } @@ -440,12 +479,12 @@ public: virtual void setValue(Ogre::Real time) { - if(mRotations.mKeys.size() > 0) - mNode->setOrientation(interpKey(mRotations.mKeys, time)); - if(mTranslations.mKeys.size() > 0) - mNode->setPosition(interpKey(mTranslations.mKeys, time)); - if(mScales.mKeys.size() > 0) - mNode->setScale(Ogre::Vector3(interpKey(mScales.mKeys, time))); + if(mRotations->mKeys.size() > 0) + mNode->setOrientation(interpKey(mRotations->mKeys, time)); + if(mTranslations->mKeys.size() > 0) + mNode->setPosition(interpKey(mTranslations->mKeys, time)); + if(mScales->mKeys.size() > 0) + mNode->setScale(Ogre::Vector3(interpKey(mScales->mKeys, time))); } }; @@ -459,10 +498,10 @@ public: { private: Ogre::MovableObject* mMovable; - Nif::FloatKeyList mUTrans; - Nif::FloatKeyList mVTrans; - Nif::FloatKeyList mUScale; - Nif::FloatKeyList mVScale; + Nif::FloatKeyMap mUTrans; + Nif::FloatKeyMap mVTrans; + Nif::FloatKeyMap mUScale; + Nif::FloatKeyMap mVScale; MaterialControllerManager* mMaterialControllerMgr; public: @@ -756,9 +795,14 @@ class NIFObjectLoader Ogre::ParticleEmitter *emitter = partsys->addEmitter("Nif"); emitter->setParticleVelocity(partctrl->velocity - partctrl->velocityRandom*0.5f, partctrl->velocity + partctrl->velocityRandom*0.5f); - emitter->setEmissionRate(partctrl->emitRate); - emitter->setTimeToLive(partctrl->lifetime, - partctrl->lifetime + partctrl->lifetimeRandom); + + if (partctrl->emitFlags & Nif::NiParticleSystemController::NoAutoAdjust) + emitter->setEmissionRate(partctrl->emitRate); + else + emitter->setEmissionRate(partctrl->numParticles / (partctrl->lifetime + partctrl->lifetimeRandom/2)); + + emitter->setTimeToLive(std::max(0.f, partctrl->lifetime), + std::max(0.f, partctrl->lifetime + partctrl->lifetimeRandom)); emitter->setParameter("width", Ogre::StringConverter::toString(partctrl->offsetRandom.x)); emitter->setParameter("height", Ogre::StringConverter::toString(partctrl->offsetRandom.y)); emitter->setParameter("depth", Ogre::StringConverter::toString(partctrl->offsetRandom.z)); @@ -796,18 +840,19 @@ class NIFObjectLoader const Nif::NiColorData *clrdata = cl->data.getPtr(); Ogre::ParticleAffector *affector = partsys->addAffector("ColourInterpolator"); - size_t num_colors = std::min(6, clrdata->mKeyList.mKeys.size()); - for(size_t i = 0;i < num_colors;i++) + size_t num_colors = std::min(6, clrdata->mKeyMap.mKeys.size()); + unsigned int i=0; + for (Nif::Vector4KeyMap::MapType::const_iterator it = clrdata->mKeyMap.mKeys.begin(); it != clrdata->mKeyMap.mKeys.end() && i < num_colors; ++it,++i) { Ogre::ColourValue color; - color.r = clrdata->mKeyList.mKeys[i].mValue[0]; - color.g = clrdata->mKeyList.mKeys[i].mValue[1]; - color.b = clrdata->mKeyList.mKeys[i].mValue[2]; - color.a = clrdata->mKeyList.mKeys[i].mValue[3]; + color.r = it->second.mValue[0]; + color.g = it->second.mValue[1]; + color.b = it->second.mValue[2]; + color.a = it->second.mValue[3]; affector->setParameter("colour"+Ogre::StringConverter::toString(i), Ogre::StringConverter::toString(color)); affector->setParameter("time"+Ogre::StringConverter::toString(i), - Ogre::StringConverter::toString(clrdata->mKeyList.mKeys[i].mTime)); + Ogre::StringConverter::toString(it->first)); } } else if(e->recType == Nif::RC_NiParticleRotation) @@ -850,7 +895,10 @@ class NIFObjectLoader partsys->setMaterialName(NIFMaterialLoader::getMaterial(particledata, fullname, group, texprop, matprop, alphaprop, vertprop, zprop, specprop, - wireprop, needTangents)); + wireprop, needTangents, + // MW doesn't light particles, but the MaterialProperty + // used still has lighting, so that must be ignored. + true)); partsys->setCullIndividually(false); partsys->setParticleQuota(particledata->numParticles); @@ -874,9 +922,28 @@ class NIFObjectLoader { int trgtid = NIFSkeletonLoader::lookupOgreBoneHandle(name, partctrl->emitter->recIndex); Ogre::Bone *trgtbone = scene->mSkelBase->getSkeleton()->getBone(trgtid); - // Set the emitter bone as user data on the particle system + // Set the emitter bone(s) as user data on the particle system // so the emitters/affectors can access it easily. - partsys->getUserObjectBindings().setUserAny(Ogre::Any(trgtbone)); + std::vector bones; + if (partctrl->recType == Nif::RC_NiBSPArrayController) + { + std::vector nodes; + getAllNiNodes(partctrl->emitter.getPtr(), nodes); + if (nodes.empty()) + throw std::runtime_error("Emitter for NiBSPArrayController must be a NiNode"); + for (unsigned int i=0; imSkelBase->getSkeleton()->getBone( + NIFSkeletonLoader::lookupOgreBoneHandle(name, nodes[i]->recIndex))); + } + } + else + { + bones.push_back(trgtbone); + } + NiNodeHolder holder; + holder.mBones = bones; + partsys->getUserObjectBindings().setUserAny(Ogre::Any(holder)); createParticleEmitterAffectors(partsys, partctrl, trgtbone, scene->mSkelBase->getName()); } @@ -892,8 +959,10 @@ class NIFObjectLoader scene->mControllers.push_back(Ogre::Controller(srcval, dstval, func)); - if (partflags&Nif::NiNode::ParticleFlag_AutoPlay) - partsys->fastForward(1, 0.1); + // Emitting state will be overwritten on frame update by the ParticleSystemController, + // but set up an initial value anyway so the user can fast-forward particle systems + // immediately after creation if desired. + partsys->setEmitting(partflags&Nif::NiNode::ParticleFlag_AutoPlay); } ctrl = ctrl->next; } @@ -905,7 +974,7 @@ class NIFObjectLoader } - static void createNodeControllers(const std::string &name, Nif::ControllerPtr ctrl, ObjectScenePtr scene, int animflags) + static void createNodeControllers(const Nif::NIFFilePtr& nif, const std::string &name, Nif::ControllerPtr ctrl, ObjectScenePtr scene, int animflags) { do { if (ctrl->flags & Nif::NiNode::ControllerFlag_Active) @@ -939,7 +1008,7 @@ class NIFObjectLoader Ogre::ControllerValueRealPtr srcval((animflags&Nif::NiNode::AnimFlag_AutoPlay) ? Ogre::ControllerManager::getSingleton().getFrameTimeSource() : Ogre::ControllerValueRealPtr()); - Ogre::ControllerValueRealPtr dstval(OGRE_NEW KeyframeController::Value(trgtbone, key->data.getPtr())); + Ogre::ControllerValueRealPtr dstval(OGRE_NEW KeyframeController::Value(trgtbone, nif, key->data.getPtr())); KeyframeController::Function* function = OGRE_NEW KeyframeController::Function(key, (animflags&Nif::NiNode::AnimFlag_AutoPlay)); scene->mMaxControllerLength = std::max(function->mStopTime, scene->mMaxControllerLength); Ogre::ControllerFunctionRealPtr func(function); @@ -979,7 +1048,7 @@ class NIFObjectLoader { std::string::const_iterator last = str.end(); do { - last--; + --last; } while(last != str.begin() && ::isspace(*last)); nextpos = std::distance(str.begin(), ++last); } @@ -992,13 +1061,13 @@ class NIFObjectLoader } - static void createObjects(const std::string &name, const std::string &group, + static void createObjects(const Nif::NIFFilePtr& nif, const std::string &name, const std::string &group, Ogre::SceneNode *sceneNode, const Nif::Node *node, - ObjectScenePtr scene, int flags, int animflags, int partflags) + ObjectScenePtr scene, int flags, int animflags, int partflags, bool isRootCollisionNode=false) { // Do not create objects for the collision shape (includes all children) if(node->recType == Nif::RC_RootCollisionNode) - return; + isRootCollisionNode = true; // Marker objects: just skip the entire node branch /// \todo don't do this in the editor @@ -1030,11 +1099,7 @@ class NIFObjectLoader { const Nif::NiTextKeyExtraData *tk = static_cast(e.getPtr()); - if (scene->mSkelBase) - { - int trgtid = NIFSkeletonLoader::lookupOgreBoneHandle(name, node->recIndex); - extractTextKeys(tk, scene->mTextKeys[trgtid]); - } + extractTextKeys(tk, scene->mTextKeys); } else if(e->recType == Nif::RC_NiStringExtraData) { @@ -1053,22 +1118,25 @@ class NIFObjectLoader } if(!node->controller.empty()) - createNodeControllers(name, node->controller, scene, animflags); + createNodeControllers(nif, name, node->controller, scene, animflags); - if(node->recType == Nif::RC_NiCamera) + if (!isRootCollisionNode) { - /* Ignored */ - } + if(node->recType == Nif::RC_NiCamera) + { + /* Ignored */ + } - if(node->recType == Nif::RC_NiTriShape && !(flags&0x80000000)) - { - createEntity(name, group, sceneNode->getCreator(), scene, node, flags, animflags); - } + if(node->recType == Nif::RC_NiTriShape && !(flags&0x80000000)) + { + createEntity(name, group, sceneNode->getCreator(), scene, node, flags, animflags); + } - if((node->recType == Nif::RC_NiAutoNormalParticles || - node->recType == Nif::RC_NiRotatingParticles) && !(flags&0x40000000)) - { - createParticleSystem(name, group, sceneNode, scene, node, flags, partflags, animflags); + if((node->recType == Nif::RC_NiAutoNormalParticles || + node->recType == Nif::RC_NiRotatingParticles) && !(flags&0x40000000)) + { + createParticleSystem(name, group, sceneNode, scene, node, flags, partflags, animflags); + } } const Nif::NiNode *ninode = dynamic_cast(node); @@ -1078,7 +1146,7 @@ class NIFObjectLoader for(size_t i = 0;i < children.length();i++) { if(!children[i].empty()) - createObjects(name, group, sceneNode, children[i].getPtr(), scene, flags, animflags, partflags); + createObjects(nif, name, group, sceneNode, children[i].getPtr(), scene, flags, animflags, partflags, isRootCollisionNode); } } } @@ -1101,7 +1169,7 @@ class NIFObjectLoader public: static void load(Ogre::SceneNode *sceneNode, ObjectScenePtr scene, const std::string &name, const std::string &group, int flags=0) { - Nif::NIFFile::ptr nif = Nif::NIFFile::create(name); + Nif::NIFFilePtr nif = Nif::Cache::getInstance().load(name); if(nif->numRoots() < 1) { nif->warn("Found no root nodes in "+name+"."); @@ -1125,13 +1193,13 @@ public: // Create a base skeleton entity if this NIF needs one createSkelBase(name, group, sceneNode->getCreator(), node, scene); } - createObjects(name, group, sceneNode, node, scene, flags, 0, 0); + createObjects(nif, name, group, sceneNode, node, scene, flags, 0, 0); } static void loadKf(Ogre::Skeleton *skel, const std::string &name, TextKeyMap &textKeys, std::vector > &ctrls) { - Nif::NIFFile::ptr nif = Nif::NIFFile::create(name); + Nif::NIFFilePtr nif = Nif::Cache::getInstance().load(name); if(nif->numRoots() < 1) { nif->warn("Found no root nodes in "+name+"."); @@ -1182,7 +1250,7 @@ public: Ogre::Bone *trgtbone = skel->getBone(strdata->string); Ogre::ControllerValueRealPtr srcval; - Ogre::ControllerValueRealPtr dstval(OGRE_NEW KeyframeController::Value(trgtbone, key->data.getPtr())); + Ogre::ControllerValueRealPtr dstval(OGRE_NEW KeyframeController::Value(trgtbone, nif, key->data.getPtr())); Ogre::ControllerFunctionRealPtr func(OGRE_NEW KeyframeController::Function(key, false)); ctrls.push_back(Ogre::Controller(srcval, dstval, func)); @@ -1193,7 +1261,7 @@ public: ObjectScenePtr Loader::createObjects(Ogre::SceneNode *parentNode, std::string name, const std::string &group) { - ObjectScenePtr scene = ObjectScenePtr (new ObjectScene(parentNode->getCreator()));; + ObjectScenePtr scene = ObjectScenePtr (new ObjectScene(parentNode->getCreator())); Misc::StringUtils::toLower(name); NIFObjectLoader::load(parentNode, scene, name, group); diff --git a/components/nifogre/ogrenifloader.hpp b/components/nifogre/ogrenifloader.hpp index badb6ccd3..abadd38de 100644 --- a/components/nifogre/ogrenifloader.hpp +++ b/components/nifogre/ogrenifloader.hpp @@ -69,7 +69,7 @@ struct ObjectScene { // The maximum length on any of the controllers. For animations with controllers, but no text keys, consider this the animation length. float mMaxControllerLength; - std::map mTextKeys; + TextKeyMap mTextKeys; MaterialControllerManager mMaterialControllerMgr; @@ -82,6 +82,8 @@ struct ObjectScene { // Rotate nodes in mBillboardNodes so they face the given camera void rotateBillboardNodes(Ogre::Camera* camera); + + void setVisibilityFlags (unsigned int flags); }; typedef Ogre::SharedPtr ObjectScenePtr; diff --git a/components/nifogre/particles.cpp b/components/nifogre/particles.cpp index a1433a669..316e4edc2 100644 --- a/components/nifogre/particles.cpp +++ b/components/nifogre/particles.cpp @@ -16,7 +16,7 @@ class NifEmitter : public Ogre::ParticleEmitter { public: - Ogre::Bone* mEmitterBone; + std::vector mEmitterBones; Ogre::Bone* mParticleBone; Ogre::ParticleSystem* getPartSys() { return mParent; } @@ -130,8 +130,9 @@ public: NifEmitter(Ogre::ParticleSystem *psys) : Ogre::ParticleEmitter(psys) + , mEmitterBones(Ogre::any_cast(psys->getUserObjectBindings().getUserAny()).mBones) { - mEmitterBone = Ogre::any_cast(psys->getUserObjectBindings().getUserAny()); + assert (!mEmitterBones.empty()); Ogre::TagPoint* tag = static_cast(mParent->getParentNode()); mParticleBone = static_cast(tag->getParent()); initDefaults("Nif"); @@ -170,8 +171,10 @@ public: Ogre::Real& timeToLive = particle->timeToLive; #endif + Ogre::Node* emitterBone = mEmitterBones.at((int)(::rand()/(RAND_MAX+1.0)*mEmitterBones.size())); + position = xOff + yOff + zOff + - mParticleBone->_getDerivedOrientation().Inverse() * (mEmitterBone->_getDerivedPosition() + mParticleBone->_getDerivedOrientation().Inverse() * (emitterBone->_getDerivedPosition() - mParticleBone->_getDerivedPosition()); // Generate complex data by reference @@ -181,7 +184,7 @@ public: Ogre::Radian hdir = mHorizontalDir + mHorizontalAngle*Ogre::Math::SymmetricRandom(); Ogre::Radian vdir = mVerticalDir + mVerticalAngle*Ogre::Math::SymmetricRandom(); direction = (mParticleBone->_getDerivedOrientation().Inverse() - * mEmitterBone->_getDerivedOrientation() * + * emitterBone->_getDerivedOrientation() * Ogre::Quaternion(hdir, Ogre::Vector3::UNIT_Z) * Ogre::Quaternion(vdir, Ogre::Vector3::UNIT_X)) * Ogre::Vector3::UNIT_Z; @@ -448,12 +451,14 @@ public: if(life_time-particle_time < mGrowTime) { Ogre::Real scale = (life_time-particle_time) / mGrowTime; + assert (scale >= 0); width *= scale; height *= scale; } if(particle_time < mFadeTime) { Ogre::Real scale = particle_time / mFadeTime; + assert (scale >= 0); width *= scale; height *= scale; } @@ -479,12 +484,14 @@ public: if(life_time-particle_time < mGrowTime) { Ogre::Real scale = (life_time-particle_time) / mGrowTime; + assert (scale >= 0); width *= scale; height *= scale; } if(particle_time < mFadeTime) { Ogre::Real scale = particle_time / mFadeTime; + assert (scale >= 0); width *= scale; height *= scale; } @@ -631,7 +638,9 @@ public: , mPosition(0.0f) , mDirection(0.0f) { - mEmitterBone = Ogre::any_cast(psys->getUserObjectBindings().getUserAny()); + std::vector bones = Ogre::any_cast(psys->getUserObjectBindings().getUserAny()).mBones; + assert (!bones.empty()); + mEmitterBone = bones[0]; Ogre::TagPoint* tag = static_cast(mParent->getParentNode()); mParticleBone = static_cast(tag->getParent()); diff --git a/components/nifogre/particles.hpp b/components/nifogre/particles.hpp index e1f3fd282..6efc669fe 100644 --- a/components/nifogre/particles.hpp +++ b/components/nifogre/particles.hpp @@ -38,4 +38,13 @@ class GravityAffectorFactory : public Ogre::ParticleAffectorFactory Ogre::ParticleAffector *createAffector(Ogre::ParticleSystem *psys); }; +struct NiNodeHolder +{ + std::vector mBones; + + // Ogre::Any needs this for some reason + friend std::ostream& operator<<(std::ostream& o, const NiNodeHolder& r) + { return o; } +}; + #endif /* OENGINE_OGRE_PARTICLES_H */ diff --git a/components/nifogre/skeleton.cpp b/components/nifogre/skeleton.cpp index 26647e595..4f6921d89 100644 --- a/components/nifogre/skeleton.cpp +++ b/components/nifogre/skeleton.cpp @@ -6,6 +6,7 @@ #include #include +#include #include namespace NifOgre @@ -14,16 +15,38 @@ namespace NifOgre void NIFSkeletonLoader::buildBones(Ogre::Skeleton *skel, const Nif::Node *node, Ogre::Bone *parent) { Ogre::Bone *bone; - if(!skel->hasBone(node->name)) - bone = skel->createBone(node->name); + if (node->name.empty()) + { + // HACK: use " " instead of empty name, otherwise Ogre will replace it with an auto-generated + // name in SkeletonInstance::cloneBoneAndChildren. + static const char* emptyname = " "; + if (!skel->hasBone(emptyname)) + bone = skel->createBone(emptyname); + else + bone = skel->createBone(); + } else - bone = skel->createBone(); + { + if(!skel->hasBone(node->name)) + bone = skel->createBone(node->name); + else + bone = skel->createBone(); + } + if(parent) parent->addChild(bone); mNifToOgreHandleMap[node->recIndex] = bone->getHandle(); - bone->setOrientation(node->trafo.rotation); - bone->setPosition(node->trafo.pos); - bone->setScale(Ogre::Vector3(node->trafo.scale)); + // decompose the local transform into position, scale and orientation. + // this is required for cases where the rotationScale matrix includes scaling, which the NIF format allows :( + // the code would look a bit nicer if Ogre allowed setting the transform matrix of a Bone directly, but we can't do that. + Ogre::Matrix4 mat(node->getLocalTransform()); + Ogre::Vector3 position, scale; + Ogre::Quaternion orientation; + mat.decomposition(position, scale, orientation); + bone->setOrientation(orientation); + bone->setPosition(position); + bone->setScale(scale); + bone->setBindingPose(); if(!(node->recType == Nif::RC_NiNode || /* Nothing special; children traversed below */ @@ -69,7 +92,7 @@ void NIFSkeletonLoader::loadResource(Ogre::Resource *resource) Ogre::Skeleton *skel = dynamic_cast(resource); OgreAssert(skel, "Attempting to load a skeleton into a non-skeleton resource!"); - Nif::NIFFile::ptr nif(Nif::NIFFile::create(skel->getName())); + Nif::NIFFilePtr nif(Nif::Cache::getInstance().load(skel->getName())); const Nif::Node *node = static_cast(nif->getRoot(0)); try { diff --git a/components/ogreinit/ogreinit.cpp b/components/ogreinit/ogreinit.cpp index 77dbcb1ee..40712e282 100644 --- a/components/ogreinit/ogreinit.cpp +++ b/components/ogreinit/ogreinit.cpp @@ -22,6 +22,7 @@ #include "ogreplugin.hpp" + namespace bfs = boost::filesystem; namespace @@ -44,7 +45,7 @@ namespace LogListener(const std::string &path) : file((bfs::path(path))) { - memset(buffer, sizeof(buffer), 0); + memset(buffer, 0, sizeof(buffer)); } void timestamp() @@ -82,28 +83,34 @@ namespace OgreInit #ifdef ENABLE_PLUGIN_GL , mGLPlugin(NULL) #endif - #ifdef ENABLE_PLUGIN_Direct3D9 + #ifdef ENABLE_PLUGIN_GLES2 + , mGLES2Plugin(NULL) + #endif + + #ifdef ENABLE_PLUGIN_Direct3D9 , mD3D9Plugin(NULL) #endif {} Ogre::Root* OgreInit::init(const std::string &logPath) { + + #ifndef ANDROID // Set up logging first new Ogre::LogManager; Ogre::Log *log = Ogre::LogManager::getSingleton().createLog(logPath); - #if OGRE_PLATFORM == OGRE_PLATFORM_WIN32 + #if OGRE_PLATFORM == OGRE_PLATFORM_WIN32 // Use custom listener only on Windows log->addListener(new LogListener(logPath)); - #endif + #endif // Disable logging to cout/cerr log->setDebugOutputEnabled(false); - + #endif mRoot = new Ogre::Root("", "", ""); - #if defined(ENABLE_PLUGIN_GL) || defined(ENABLE_PLUGIN_Direct3D9) || defined(ENABLE_PLUGIN_CgProgramManager) || defined(ENABLE_PLUGIN_OctreeSceneManager) || defined(ENABLE_PLUGIN_ParticleFX) + #if defined(ENABLE_PLUGIN_GL) || (ENABLE_PLUGIN_GLES2) || defined(ENABLE_PLUGIN_Direct3D9) || defined(ENABLE_PLUGIN_CgProgramManager) || defined(ENABLE_PLUGIN_OctreeSceneManager) || defined(ENABLE_PLUGIN_ParticleFX) loadStaticPlugins(); #else loadPlugins(); @@ -133,6 +140,10 @@ namespace OgreInit delete mGLPlugin; mGLPlugin = NULL; #endif + #ifdef ENABLE_PLUGIN_GLES2 + delete mGLES2Plugin; + mGLES2Plugin = NULL; + #endif #ifdef ENABLE_PLUGIN_Direct3D9 delete mD3D9Plugin; mD3D9Plugin = NULL; @@ -157,6 +168,10 @@ namespace OgreInit mGLPlugin = new Ogre::GLPlugin(); mRoot->installPlugin(mGLPlugin); #endif + #ifdef ENABLE_PLUGIN_GLES2 + mGLES2Plugin = new Ogre::GLES2Plugin(); + mRoot->installPlugin(mGLES2Plugin); + #endif #ifdef ENABLE_PLUGIN_Direct3D9 mD3D9Plugin = new Ogre::D3D9Plugin(); mRoot->installPlugin(mD3D9Plugin); @@ -193,7 +208,7 @@ namespace OgreInit pluginDir = Ogre::macFrameworksPath(); #endif #if OGRE_PLATFORM == OGRE_PLATFORM_LINUX - pluginDir = OGRE_PLUGIN_DIR_REL; + pluginDir = OGRE_PLUGIN_DIR; #endif } Files::loadOgrePlugin(pluginDir, "RenderSystem_GL", *mRoot); @@ -201,7 +216,8 @@ namespace OgreInit Files::loadOgrePlugin(pluginDir, "RenderSystem_GL3Plus", *mRoot); Files::loadOgrePlugin(pluginDir, "RenderSystem_Direct3D9", *mRoot); Files::loadOgrePlugin(pluginDir, "Plugin_CgProgramManager", *mRoot); - Files::loadOgrePlugin(pluginDir, "Plugin_ParticleFX", *mRoot); + if (!Files::loadOgrePlugin(pluginDir, "Plugin_ParticleFX", *mRoot)) + throw std::runtime_error("Required Plugin_ParticleFX for Ogre not found!"); } void OgreInit::loadParticleFactories() diff --git a/components/ogreinit/ogreinit.hpp b/components/ogreinit/ogreinit.hpp index b6fe4631a..9613421f7 100644 --- a/components/ogreinit/ogreinit.hpp +++ b/components/ogreinit/ogreinit.hpp @@ -17,6 +17,10 @@ #ifdef ENABLE_PLUGIN_GL # include "OgreGLPlugin.h" #endif +#ifdef ENABLE_PLUGIN_GLES2 +# include "OgreGLES2Plugin.h" +#endif + #ifdef ENABLE_PLUGIN_Direct3D9 # include "OgreD3D9Plugin.h" #endif @@ -52,7 +56,6 @@ namespace OgreInit void loadPlugins(); void loadParticleFactories(); - #ifdef ENABLE_PLUGIN_CgProgramManager Ogre::CgPlugin* mCgPlugin; #endif @@ -65,6 +68,9 @@ namespace OgreInit #ifdef ENABLE_PLUGIN_GL Ogre::GLPlugin* mGLPlugin; #endif + #ifdef ENABLE_PLUGIN_GLES2 + Ogre::GLES2Plugin* mGLES2Plugin; + #endif #ifdef ENABLE_PLUGIN_Direct3D9 Ogre::D3D9Plugin* mD3D9Plugin; #endif diff --git a/components/ogreinit/ogreplugin.cpp b/components/ogreinit/ogreplugin.cpp index 6070c43a8..069b25e7b 100644 --- a/components/ogreinit/ogreplugin.cpp +++ b/components/ogreinit/ogreplugin.cpp @@ -42,4 +42,4 @@ bool loadOgrePlugin(const std::string &pluginDir, std::string pluginName, Ogre:: } } -} \ No newline at end of file +} diff --git a/components/settings/settings.cpp b/components/settings/settings.cpp index 0def0afdb..5fc2ca3c1 100644 --- a/components/settings/settings.cpp +++ b/components/settings/settings.cpp @@ -79,7 +79,7 @@ void Manager::saveUser(const std::string& file) fout.close(); } -const std::string Manager::getString (const std::string& setting, const std::string& category) +std::string Manager::getString (const std::string& setting, const std::string& category) { if (mNewSettings.find(std::make_pair(category, setting)) != mNewSettings.end()) return mNewSettings[std::make_pair(category, setting)]; @@ -92,17 +92,17 @@ const std::string Manager::getString (const std::string& setting, const std::str return val; } -const float Manager::getFloat (const std::string& setting, const std::string& category) +float Manager::getFloat (const std::string& setting, const std::string& category) { return Ogre::StringConverter::parseReal( getString(setting, category) ); } -const int Manager::getInt (const std::string& setting, const std::string& category) +int Manager::getInt (const std::string& setting, const std::string& category) { return Ogre::StringConverter::parseInt( getString(setting, category) ); } -const bool Manager::getBool (const std::string& setting, const std::string& category) +bool Manager::getBool (const std::string& setting, const std::string& category) { return Ogre::StringConverter::parseBool( getString(setting, category) ); } diff --git a/components/settings/settings.hpp b/components/settings/settings.hpp index b7c7d59a9..03b0b517c 100644 --- a/components/settings/settings.hpp +++ b/components/settings/settings.hpp @@ -36,10 +36,10 @@ namespace Settings static const CategorySettingVector apply(); ///< returns the list of changed settings and then clears it - static const int getInt (const std::string& setting, const std::string& category); - static const float getFloat (const std::string& setting, const std::string& category); - static const std::string getString (const std::string& setting, const std::string& category); - static const bool getBool (const std::string& setting, const std::string& category); + static int getInt (const std::string& setting, const std::string& category); + static float getFloat (const std::string& setting, const std::string& category); + static std::string getString (const std::string& setting, const std::string& category); + static bool getBool (const std::string& setting, const std::string& category); static void setInt (const std::string& setting, const std::string& category, const int value); static void setFloat (const std::string& setting, const std::string& category, const float value); diff --git a/components/terrain/buffercache.cpp b/components/terrain/buffercache.cpp index f693c0e40..a3e67af5b 100644 --- a/components/terrain/buffercache.cpp +++ b/components/terrain/buffercache.cpp @@ -118,7 +118,7 @@ namespace Terrain // North row = verts-1; - outerStep = 1 << (lodDeltas[North] + lodLevel); + outerStep = size_t(1) << (lodDeltas[North] + lodLevel); for (size_t col = 0; col < verts-1; col += outerStep) { indices.push_back(verts*(col+outerStep)+row); @@ -142,7 +142,7 @@ namespace Terrain // West size_t col = 0; - outerStep = 1 << (lodDeltas[West] + lodLevel); + outerStep = size_t(1) << (lodDeltas[West] + lodLevel); for (size_t row = 0; row < verts-1; row += outerStep) { indices.push_back(verts*col+row+outerStep); @@ -166,7 +166,7 @@ namespace Terrain // East col = verts-1; - outerStep = 1 << (lodDeltas[East] + lodLevel); + outerStep = size_t(1) << (lodDeltas[East] + lodLevel); for (size_t row = 0; row < verts-1; row += outerStep) { indices.push_back(verts*col+row); diff --git a/components/terrain/chunk.cpp b/components/terrain/chunk.cpp index 0820dcc73..9c60ee017 100644 --- a/components/terrain/chunk.cpp +++ b/components/terrain/chunk.cpp @@ -4,21 +4,21 @@ #include #include #include +#include #include - -#include "world.hpp" // FIXME: for LoadResponseData, move to backgroundloader.hpp - namespace Terrain { - Chunk::Chunk(Ogre::HardwareVertexBufferSharedPtr uvBuffer, const Ogre::AxisAlignedBox& bounds, const LoadResponseData& data) + Chunk::Chunk(Ogre::HardwareVertexBufferSharedPtr uvBuffer, const Ogre::AxisAlignedBox& bounds, + const std::vector& positions, const std::vector& normals, const std::vector& colours) : mBounds(bounds) + , mOwnMaterial(false) { mVertexData = OGRE_NEW Ogre::VertexData; mVertexData->vertexStart = 0; - mVertexData->vertexCount = data.mPositions.size()/3; + mVertexData->vertexCount = positions.size()/3; // Set up the vertex declaration, which specifies the info for each vertex (normals, colors, UVs, etc) Ogre::VertexDeclaration* vertexDecl = mVertexData->vertexDeclaration; @@ -46,15 +46,18 @@ namespace Terrain Ogre::HardwareVertexBufferSharedPtr colourBuffer = mgr->createVertexBuffer(Ogre::VertexElement::getTypeSize(Ogre::VET_COLOUR), mVertexData->vertexCount, Ogre::HardwareBuffer::HBU_STATIC); - vertexBuffer->writeData(0, vertexBuffer->getSizeInBytes(), &data.mPositions[0], true); - normalBuffer->writeData(0, normalBuffer->getSizeInBytes(), &data.mNormals[0], true); - colourBuffer->writeData(0, colourBuffer->getSizeInBytes(), &data.mColours[0], true); + vertexBuffer->writeData(0, vertexBuffer->getSizeInBytes(), &positions[0], true); + normalBuffer->writeData(0, normalBuffer->getSizeInBytes(), &normals[0], true); + colourBuffer->writeData(0, colourBuffer->getSizeInBytes(), &colours[0], true); mVertexData->vertexBufferBinding->setBinding(0, vertexBuffer); mVertexData->vertexBufferBinding->setBinding(1, normalBuffer); mVertexData->vertexBufferBinding->setBinding(2, uvBuffer); mVertexData->vertexBufferBinding->setBinding(3, colourBuffer); + // Assign a default material in case terrain material fails to be created + mMaterial = Ogre::MaterialManager::getSingleton().getByName("BaseWhite"); + mIndexData = OGRE_NEW Ogre::IndexData(); mIndexData->indexStart = 0; } @@ -67,18 +70,30 @@ namespace Terrain Chunk::~Chunk() { + if (!mMaterial.isNull() && mOwnMaterial) + { #if TERRAIN_USE_SHADER - sh::Factory::getInstance().destroyMaterialInstance(mMaterial->getName()); + sh::Factory::getInstance().destroyMaterialInstance(mMaterial->getName()); #endif - Ogre::MaterialManager::getSingleton().remove(mMaterial->getName()); - + Ogre::MaterialManager::getSingleton().remove(mMaterial->getName()); + } OGRE_DELETE mVertexData; OGRE_DELETE mIndexData; } - void Chunk::setMaterial(const Ogre::MaterialPtr &material) + void Chunk::setMaterial(const Ogre::MaterialPtr &material, bool own) { + // Clean up the previous material, if we own it + if (!mMaterial.isNull() && mOwnMaterial) + { +#if TERRAIN_USE_SHADER + sh::Factory::getInstance().destroyMaterialInstance(mMaterial->getName()); +#endif + Ogre::MaterialManager::getSingleton().remove(mMaterial->getName()); + } + mMaterial = material; + mOwnMaterial = own; } const Ogre::AxisAlignedBox& Chunk::getBoundingBox(void) const diff --git a/components/terrain/chunk.hpp b/components/terrain/chunk.hpp index d037661ae..9b2ed76ac 100644 --- a/components/terrain/chunk.hpp +++ b/components/terrain/chunk.hpp @@ -7,21 +7,21 @@ namespace Terrain { - class BufferCache; - struct LoadResponseData; - /** - * @brief Renders a chunk of terrain, either using alpha splatting or a composite map. + * @brief A movable object representing a chunk of terrain. */ class Chunk : public Ogre::Renderable, public Ogre::MovableObject { public: - Chunk (Ogre::HardwareVertexBufferSharedPtr uvBuffer, const Ogre::AxisAlignedBox& bounds, const LoadResponseData& data); + Chunk (Ogre::HardwareVertexBufferSharedPtr uvBuffer, const Ogre::AxisAlignedBox& bounds, + const std::vector& positions, + const std::vector& normals, + const std::vector& colours); virtual ~Chunk(); - /// @note Takes ownership of \a material - void setMaterial (const Ogre::MaterialPtr& material); + /// @param own Should we take ownership of the material? + void setMaterial (const Ogre::MaterialPtr& material, bool own=true); void setIndexBuffer(Ogre::HardwareIndexBufferSharedPtr buffer); @@ -43,6 +43,7 @@ namespace Terrain private: Ogre::AxisAlignedBox mBounds; Ogre::MaterialPtr mMaterial; + bool mOwnMaterial; // Should we remove mMaterial on destruction? Ogre::VertexData* mVertexData; Ogre::IndexData* mIndexData; diff --git a/components/terrain/defaultworld.cpp b/components/terrain/defaultworld.cpp new file mode 100644 index 000000000..943658235 --- /dev/null +++ b/components/terrain/defaultworld.cpp @@ -0,0 +1,315 @@ +#include "defaultworld.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include "storage.hpp" +#include "quadtreenode.hpp" + +namespace +{ + + bool isPowerOfTwo(int x) + { + return ( (x > 0) && ((x & (x - 1)) == 0) ); + } + + int nextPowerOfTwo (int v) + { + if (isPowerOfTwo(v)) return v; + int depth=0; + while(v) + { + v >>= 1; + depth++; + } + return 1 << depth; + } + + Terrain::QuadTreeNode* findNode (const Ogre::Vector2& center, Terrain::QuadTreeNode* node) + { + if (center == node->getCenter()) + return node; + + if (center.x > node->getCenter().x && center.y > node->getCenter().y) + return findNode(center, node->getChild(Terrain::NE)); + else if (center.x > node->getCenter().x && center.y < node->getCenter().y) + return findNode(center, node->getChild(Terrain::SE)); + else if (center.x < node->getCenter().x && center.y > node->getCenter().y) + return findNode(center, node->getChild(Terrain::NW)); + else //if (center.x < node->getCenter().x && center.y < node->getCenter().y) + return findNode(center, node->getChild(Terrain::SW)); + } + +} + +namespace Terrain +{ + + const Ogre::uint REQ_ID_CHUNK = 1; + const Ogre::uint REQ_ID_LAYERS = 2; + + DefaultWorld::DefaultWorld(Ogre::SceneManager* sceneMgr, + Storage* storage, int visibilityFlags, bool shaders, Alignment align, float minBatchSize, float maxBatchSize) + : World(sceneMgr, storage, visibilityFlags, shaders, align) + , mMinBatchSize(minBatchSize) + , mMaxBatchSize(maxBatchSize) + , mVisible(true) + , mMaxX(0) + , mMinX(0) + , mMaxY(0) + , mMinY(0) + , mChunksLoading(0) + , mWorkQueueChannel(0) + , mLayerLoadPending(true) + { +#if TERRAIN_USE_SHADER == 0 + if (mShaders) + std::cerr << "Compiled Terrain without shader support, disabling..." << std::endl; + mShaders = false; +#endif + + mCompositeMapSceneMgr = Ogre::Root::getSingleton().createSceneManager(Ogre::ST_GENERIC); + + /// \todo make composite map size configurable + Ogre::Camera* compositeMapCam = mCompositeMapSceneMgr->createCamera("a"); + mCompositeMapRenderTexture = Ogre::TextureManager::getSingleton().createManual( + "terrain/comp/rt", Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME, + Ogre::TEX_TYPE_2D, 128, 128, 0, Ogre::PF_A8B8G8R8, Ogre::TU_RENDERTARGET); + mCompositeMapRenderTarget = mCompositeMapRenderTexture->getBuffer()->getRenderTarget(); + mCompositeMapRenderTarget->setAutoUpdated(false); + mCompositeMapRenderTarget->addViewport(compositeMapCam); + + storage->getBounds(mMinX, mMaxX, mMinY, mMaxY); + + int origSizeX = mMaxX-mMinX; + int origSizeY = mMaxY-mMinY; + + // Dividing a quad tree only works well for powers of two, so round up to the nearest one + int size = nextPowerOfTwo(std::max(origSizeX, origSizeY)); + + // Adjust the center according to the new size + float centerX = (mMinX+mMaxX)/2.f + (size-origSizeX)/2.f; + float centerY = (mMinY+mMaxY)/2.f + (size-origSizeY)/2.f; + + mRootSceneNode = mSceneMgr->getRootSceneNode()->createChildSceneNode(); + + // While building the quadtree, remember leaf nodes since we need to load their layers + LayersRequestData data; + data.mPack = getShadersEnabled(); + + mRootNode = new QuadTreeNode(this, Root, size, Ogre::Vector2(centerX, centerY), NULL); + buildQuadTree(mRootNode, data.mNodes); + //loadingListener->indicateProgress(); + mRootNode->initAabb(); + //loadingListener->indicateProgress(); + mRootNode->initNeighbours(); + //loadingListener->indicateProgress(); + + Ogre::WorkQueue* wq = Ogre::Root::getSingleton().getWorkQueue(); + mWorkQueueChannel = wq->getChannel("LargeTerrain"); + wq->addRequestHandler(mWorkQueueChannel, this); + wq->addResponseHandler(mWorkQueueChannel, this); + + // Start loading layers in the background (for leaf nodes) + wq->addRequest(mWorkQueueChannel, REQ_ID_LAYERS, Ogre::Any(data)); + } + + DefaultWorld::~DefaultWorld() + { + Ogre::WorkQueue* wq = Ogre::Root::getSingleton().getWorkQueue(); + wq->removeRequestHandler(mWorkQueueChannel, this); + wq->removeResponseHandler(mWorkQueueChannel, this); + + delete mRootNode; + } + + void DefaultWorld::buildQuadTree(QuadTreeNode *node, std::vector& leafs) + { + float halfSize = node->getSize()/2.f; + + if (node->getSize() <= mMinBatchSize) + { + // We arrived at a leaf + float minZ,maxZ; + Ogre::Vector2 center = node->getCenter(); + float cellWorldSize = getStorage()->getCellWorldSize(); + if (mStorage->getMinMaxHeights(node->getSize(), center, minZ, maxZ)) + { + Ogre::AxisAlignedBox bounds(Ogre::Vector3(-halfSize*cellWorldSize, -halfSize*cellWorldSize, minZ), + Ogre::Vector3(halfSize*cellWorldSize, halfSize*cellWorldSize, maxZ)); + convertBounds(bounds); + node->setBoundingBox(bounds); + leafs.push_back(node); + } + else + node->markAsDummy(); // no data available for this node, skip it + return; + } + + if (node->getCenter().x - halfSize > mMaxX + || node->getCenter().x + halfSize < mMinX + || node->getCenter().y - halfSize > mMaxY + || node->getCenter().y + halfSize < mMinY ) + // Out of bounds of the actual terrain - this will happen because + // we rounded the size up to the next power of two + { + node->markAsDummy(); + return; + } + + // Not a leaf, create its children + node->createChild(SW, halfSize, node->getCenter() - halfSize/2.f); + node->createChild(SE, halfSize, node->getCenter() + Ogre::Vector2(halfSize/2.f, -halfSize/2.f)); + node->createChild(NW, halfSize, node->getCenter() + Ogre::Vector2(-halfSize/2.f, halfSize/2.f)); + node->createChild(NE, halfSize, node->getCenter() + halfSize/2.f); + buildQuadTree(node->getChild(SW), leafs); + buildQuadTree(node->getChild(SE), leafs); + buildQuadTree(node->getChild(NW), leafs); + buildQuadTree(node->getChild(NE), leafs); + + // if all children are dummy, we are also dummy + for (int i=0; i<4; ++i) + { + if (!node->getChild((ChildDirection)i)->isDummy()) + return; + } + node->markAsDummy(); + } + + void DefaultWorld::update(const Ogre::Vector3& cameraPos) + { + if (!mVisible) + return; + mRootNode->update(cameraPos); + mRootNode->updateIndexBuffers(); + } + + Ogre::AxisAlignedBox DefaultWorld::getWorldBoundingBox (const Ogre::Vector2& center) + { + if (center.x > mMaxX + || center.x < mMinX + || center.y > mMaxY + || center.y < mMinY) + return Ogre::AxisAlignedBox::BOX_NULL; + QuadTreeNode* node = findNode(center, mRootNode); + return node->getWorldBoundingBox(); + } + + void DefaultWorld::renderCompositeMap(Ogre::TexturePtr target) + { + mCompositeMapRenderTarget->update(); + target->getBuffer()->blit(mCompositeMapRenderTexture->getBuffer()); + } + + void DefaultWorld::clearCompositeMapSceneManager() + { + mCompositeMapSceneMgr->destroyAllManualObjects(); + mCompositeMapSceneMgr->clearScene(); + } + + void DefaultWorld::applyMaterials(bool shadows, bool splitShadows) + { + mShadows = shadows; + mSplitShadows = splitShadows; + mRootNode->applyMaterials(); + } + + void DefaultWorld::setVisible(bool visible) + { + if (visible && !mVisible) + mSceneMgr->getRootSceneNode()->addChild(mRootSceneNode); + else if (!visible && mVisible) + mSceneMgr->getRootSceneNode()->removeChild(mRootSceneNode); + + mVisible = visible; + } + + bool DefaultWorld::getVisible() + { + return mVisible; + } + + void DefaultWorld::syncLoad() + { + while (mChunksLoading || mLayerLoadPending) + { + OGRE_THREAD_SLEEP(0); + Ogre::Root::getSingleton().getWorkQueue()->processResponses(); + } + } + + Ogre::WorkQueue::Response* DefaultWorld::handleRequest(const Ogre::WorkQueue::Request *req, const Ogre::WorkQueue *srcQ) + { + if (req->getType() == REQ_ID_CHUNK) + { + const LoadRequestData data = Ogre::any_cast(req->getData()); + + QuadTreeNode* node = data.mNode; + + LoadResponseData* responseData = new LoadResponseData(); + + getStorage()->fillVertexBuffers(node->getNativeLodLevel(), node->getSize(), node->getCenter(), getAlign(), + responseData->mPositions, responseData->mNormals, responseData->mColours); + + return OGRE_NEW Ogre::WorkQueue::Response(req, true, Ogre::Any(responseData)); + } + else // REQ_ID_LAYERS + { + const LayersRequestData data = Ogre::any_cast(req->getData()); + + LayersResponseData* responseData = new LayersResponseData(); + + getStorage()->getBlendmaps(data.mNodes, responseData->mLayerCollections, data.mPack); + + return OGRE_NEW Ogre::WorkQueue::Response(req, true, Ogre::Any(responseData)); + } + } + + void DefaultWorld::handleResponse(const Ogre::WorkQueue::Response *res, const Ogre::WorkQueue *srcQ) + { + assert(res->succeeded() && "Response failure not handled"); + + if (res->getRequest()->getType() == REQ_ID_CHUNK) + { + LoadResponseData* data = Ogre::any_cast(res->getData()); + + const LoadRequestData requestData = Ogre::any_cast(res->getRequest()->getData()); + + requestData.mNode->load(*data); + + delete data; + + --mChunksLoading; + } + else // REQ_ID_LAYERS + { + LayersResponseData* data = Ogre::any_cast(res->getData()); + + for (std::vector::iterator it = data->mLayerCollections.begin(); it != data->mLayerCollections.end(); ++it) + { + it->mTarget->loadLayers(*it); + } + + delete data; + + mRootNode->loadMaterials(); + + mLayerLoadPending = false; + } + } + + void DefaultWorld::queueLoad(QuadTreeNode *node) + { + LoadRequestData data; + data.mNode = node; + + Ogre::Root::getSingleton().getWorkQueue()->addRequest(mWorkQueueChannel, REQ_ID_CHUNK, Ogre::Any(data)); + ++mChunksLoading; + } +} diff --git a/components/terrain/defaultworld.hpp b/components/terrain/defaultworld.hpp new file mode 100644 index 000000000..62441c420 --- /dev/null +++ b/components/terrain/defaultworld.hpp @@ -0,0 +1,156 @@ +#ifndef COMPONENTS_TERRAIN_H +#define COMPONENTS_TERRAIN_H + +#include +#include +#include + +#include "world.hpp" + +namespace Ogre +{ + class Camera; +} + +namespace Terrain +{ + + class QuadTreeNode; + class Storage; + + /** + * @brief A quadtree-based terrain implementation suitable for large data sets. \n + * Near cells are rendered with alpha splatting, distant cells are merged + * together in batches and have their layers pre-rendered onto a composite map. \n + * Cracks at LOD transitions are avoided using stitching. + * @note Multiple cameras are not supported yet + */ + class DefaultWorld : public World, public Ogre::WorkQueue::RequestHandler, public Ogre::WorkQueue::ResponseHandler + { + public: + /// @note takes ownership of \a storage + /// @param sceneMgr scene manager to use + /// @param storage Storage instance to get terrain data from (heights, normals, colors, textures..) + /// @param visbilityFlags visibility flags for the created meshes + /// @param shaders Whether to use splatting shader, or multi-pass fixed function splatting. Shader is usually + /// faster so this is just here for compatibility. + /// @param align The align of the terrain, see Alignment enum + /// @param minBatchSize Minimum size of a terrain batch along one side (in cell units). Used for building the quad tree. + /// @param maxBatchSize Maximum size of a terrain batch along one side (in cell units). Used when traversing the quad tree. + DefaultWorld(Ogre::SceneManager* sceneMgr, + Storage* storage, int visibilityFlags, bool shaders, Alignment align, float minBatchSize, float maxBatchSize); + ~DefaultWorld(); + + /// Update chunk LODs according to this camera position + /// @note Calling this method might lead to composite textures being rendered, so it is best + /// not to call it when render commands are still queued, since that would cause a flush. + virtual void update (const Ogre::Vector3& cameraPos); + + /// Get the world bounding box of a chunk of terrain centered at \a center + virtual Ogre::AxisAlignedBox getWorldBoundingBox (const Ogre::Vector2& center); + + Ogre::SceneNode* getRootSceneNode() { return mRootSceneNode; } + + /// Show or hide the whole terrain + /// @note this setting will be invalidated once you call Terrain::update, so do not call it while the terrain should be hidden + virtual void setVisible(bool visible); + virtual bool getVisible(); + + /// Recreate materials used by terrain chunks. This should be called whenever settings of + /// the material factory are changed. (Relying on the factory to update those materials is not + /// enough, since turning a feature on/off can change the number of texture units available for layer/blend + /// textures, and to properly respond to this we may need to change the structure of the material, such as + /// adding or removing passes. This can only be achieved by a full rebuild.) + virtual void applyMaterials(bool shadows, bool splitShadows); + + int getMaxBatchSize() { return mMaxBatchSize; } + + /// Wait until all background loading is complete. + void syncLoad(); + + private: + // Called from a background worker thread + virtual Ogre::WorkQueue::Response* handleRequest(const Ogre::WorkQueue::Request* req, const Ogre::WorkQueue* srcQ); + // Called from the main thread + virtual void handleResponse(const Ogre::WorkQueue::Response* res, const Ogre::WorkQueue* srcQ); + Ogre::uint16 mWorkQueueChannel; + + bool mVisible; + + QuadTreeNode* mRootNode; + Ogre::SceneNode* mRootSceneNode; + + /// The number of chunks currently loading in a background thread. If 0, we have finished loading! + int mChunksLoading; + + Ogre::SceneManager* mCompositeMapSceneMgr; + + /// Bounds in cell units + float mMinX, mMaxX, mMinY, mMaxY; + + /// Minimum size of a terrain batch along one side (in cell units) + float mMinBatchSize; + /// Maximum size of a terrain batch along one side (in cell units) + float mMaxBatchSize; + + void buildQuadTree(QuadTreeNode* node, std::vector& leafs); + + // Are layers for leaf nodes loaded? This is done once at startup (but in a background thread) + bool mLayerLoadPending; + + public: + // ----INTERNAL---- + Ogre::SceneManager* getCompositeMapSceneManager() { return mCompositeMapSceneMgr; } + + bool areLayersLoaded() { return !mLayerLoadPending; } + + // Delete all quads + void clearCompositeMapSceneManager(); + void renderCompositeMap (Ogre::TexturePtr target); + + // Adds a WorkQueue request to load a chunk for this node in the background. + void queueLoad (QuadTreeNode* node); + + private: + Ogre::RenderTarget* mCompositeMapRenderTarget; + Ogre::TexturePtr mCompositeMapRenderTexture; + }; + + struct LoadRequestData + { + QuadTreeNode* mNode; + + friend std::ostream& operator<<(std::ostream& o, const LoadRequestData& r) + { return o; } + }; + + struct LoadResponseData + { + std::vector mPositions; + std::vector mNormals; + std::vector mColours; + + friend std::ostream& operator<<(std::ostream& o, const LoadResponseData& r) + { return o; } + }; + + struct LayersRequestData + { + std::vector mNodes; + bool mPack; + + friend std::ostream& operator<<(std::ostream& o, const LayersRequestData& r) + { return o; } + }; + + struct LayersResponseData + { + std::vector mLayerCollections; + + friend std::ostream& operator<<(std::ostream& o, const LayersResponseData& r) + { return o; } + }; + +} + +#endif diff --git a/components/terrain/material.cpp b/components/terrain/material.cpp index b56f70680..a40e576ed 100644 --- a/components/terrain/material.cpp +++ b/components/terrain/material.cpp @@ -36,8 +36,8 @@ std::string getBlendmapComponentForLayer (int layerIndex) namespace Terrain { - MaterialGenerator::MaterialGenerator(bool shaders) - : mShaders(shaders) + MaterialGenerator::MaterialGenerator() + : mShaders(true) , mShadows(false) , mSplitShadows(false) , mNormalMapping(true) @@ -46,35 +46,28 @@ namespace Terrain } - Ogre::MaterialPtr MaterialGenerator::generate(Ogre::MaterialPtr mat) + Ogre::MaterialPtr MaterialGenerator::generate() { assert(!mLayerList.empty() && "Can't create material with no layers"); - return create(mat, false, false); + return create(false, false); } - Ogre::MaterialPtr MaterialGenerator::generateForCompositeMapRTT(Ogre::MaterialPtr mat) + Ogre::MaterialPtr MaterialGenerator::generateForCompositeMapRTT() { assert(!mLayerList.empty() && "Can't create material with no layers"); - return create(mat, true, false); + return create(true, false); } - Ogre::MaterialPtr MaterialGenerator::generateForCompositeMap(Ogre::MaterialPtr mat) + Ogre::MaterialPtr MaterialGenerator::generateForCompositeMap() { - return create(mat, false, true); + return create(false, true); } - Ogre::MaterialPtr MaterialGenerator::create(Ogre::MaterialPtr mat, bool renderCompositeMap, bool displayCompositeMap) + Ogre::MaterialPtr MaterialGenerator::create(bool renderCompositeMap, bool displayCompositeMap) { assert(!renderCompositeMap || !displayCompositeMap); - if (!mat.isNull()) - { -#if TERRAIN_USE_SHADER - sh::Factory::getInstance().destroyMaterialInstance(mat->getName()); -#endif - Ogre::MaterialManager::getSingleton().remove(mat->getName()); - } static int count = 0; std::stringstream name; @@ -82,7 +75,7 @@ namespace Terrain if (!mShaders) { - mat = Ogre::MaterialManager::getSingleton().create(name.str(), + Ogre::MaterialPtr mat = Ogre::MaterialManager::getSingleton().create(name.str(), Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME); Ogre::Technique* technique = mat->getTechnique(0); technique->removeAllPasses(); diff --git a/components/terrain/material.hpp b/components/terrain/material.hpp index e7e067899..b9000cb1b 100644 --- a/components/terrain/material.hpp +++ b/components/terrain/material.hpp @@ -11,11 +11,7 @@ namespace Terrain class MaterialGenerator { public: - /// @param layerList layer textures - /// @param blendmapList blend textures - /// @param shaders Whether to use shaders. With a shader, blendmap packing can be used (4 channels instead of one), - /// so if this parameter is true, then the supplied blend maps are expected to be packed. - MaterialGenerator (bool shaders); + MaterialGenerator (); void setLayerList (const std::vector& layerList) { mLayerList = layerList; } bool hasLayers() { return mLayerList.size(); } @@ -23,29 +19,24 @@ namespace Terrain const std::vector& getBlendmapList() { return mBlendmapList; } void setCompositeMap (const std::string& name) { mCompositeMap = name; } + void enableShaders(bool shaders) { mShaders = shaders; } void enableShadows(bool shadows) { mShadows = shadows; } void enableNormalMapping(bool normalMapping) { mNormalMapping = normalMapping; } void enableParallaxMapping(bool parallaxMapping) { mParallaxMapping = parallaxMapping; } void enableSplitShadows(bool splitShadows) { mSplitShadows = splitShadows; } /// Creates a material suitable for displaying a chunk of terrain using alpha-blending. - /// @param mat Material that will be replaced by the generated material. May be empty as well, in which case - /// a new material is created. - Ogre::MaterialPtr generate (Ogre::MaterialPtr mat); + Ogre::MaterialPtr generate (); /// Creates a material suitable for displaying a chunk of terrain using a ready-made composite map. - /// @param mat Material that will be replaced by the generated material. May be empty as well, in which case - /// a new material is created. - Ogre::MaterialPtr generateForCompositeMap (Ogre::MaterialPtr mat); + Ogre::MaterialPtr generateForCompositeMap (); /// Creates a material suitable for rendering composite maps, i.e. for "baking" several layer textures /// into one. The main difference compared to a normal material is that no shading is applied at this point. - /// @param mat Material that will be replaced by the generated material. May be empty as well, in which case - /// a new material is created. - Ogre::MaterialPtr generateForCompositeMapRTT (Ogre::MaterialPtr mat); + Ogre::MaterialPtr generateForCompositeMapRTT (); private: - Ogre::MaterialPtr create (Ogre::MaterialPtr mat, bool renderCompositeMap, bool displayCompositeMap); + Ogre::MaterialPtr create (bool renderCompositeMap, bool displayCompositeMap); std::vector mLayerList; std::vector mBlendmapList; diff --git a/components/terrain/quadtreenode.cpp b/components/terrain/quadtreenode.cpp index a14fe1f84..57379a334 100644 --- a/components/terrain/quadtreenode.cpp +++ b/components/terrain/quadtreenode.cpp @@ -6,7 +6,7 @@ #include #include -#include "world.hpp" +#include "defaultworld.hpp" #include "chunk.hpp" #include "storage.hpp" #include "buffercache.hpp" @@ -73,38 +73,6 @@ namespace return NULL; } - - // Ogre::AxisAlignedBox::distance is broken in 1.8. - Ogre::Real distance(const Ogre::AxisAlignedBox& box, const Ogre::Vector3& v) - { - - if (box.contains(v)) - return 0; - else - { - Ogre::Vector3 maxDist(0,0,0); - const Ogre::Vector3& minimum = box.getMinimum(); - const Ogre::Vector3& maximum = box.getMaximum(); - - if (v.x < minimum.x) - maxDist.x = minimum.x - v.x; - else if (v.x > maximum.x) - maxDist.x = v.x - maximum.x; - - if (v.y < minimum.y) - maxDist.y = minimum.y - v.y; - else if (v.y > maximum.y) - maxDist.y = v.y - maximum.y; - - if (v.z < minimum.z) - maxDist.z = minimum.z - v.z; - else if (v.z > maximum.z) - maxDist.z = v.z - maximum.z; - - return maxDist.length(); - } - } - // Create a 2D quad void makeQuad(Ogre::SceneManager* sceneMgr, float left, float top, float right, float bottom, Ogre::MaterialPtr material) { @@ -142,7 +110,7 @@ namespace } } -QuadTreeNode::QuadTreeNode(World* terrain, ChildDirection dir, float size, const Ogre::Vector2 ¢er, QuadTreeNode* parent) +QuadTreeNode::QuadTreeNode(DefaultWorld* terrain, ChildDirection dir, float size, const Ogre::Vector2 ¢er, QuadTreeNode* parent) : mMaterialGenerator(NULL) , mIsDummy(false) , mSize(size) @@ -178,7 +146,8 @@ QuadTreeNode::QuadTreeNode(World* terrain, ChildDirection dir, float size, const mSceneNode->setPosition(sceneNodePos); - mMaterialGenerator = new MaterialGenerator(mTerrain->getShadersEnabled()); + mMaterialGenerator = new MaterialGenerator(); + mMaterialGenerator->enableShaders(mTerrain->getShadersEnabled()); } void QuadTreeNode::createChild(ChildDirection id, float size, const Ogre::Vector2 ¢er) @@ -269,7 +238,7 @@ bool QuadTreeNode::update(const Ogre::Vector3 &cameraPos) if (mBounds.isNull()) return true; - float dist = distance(mWorldBounds, cameraPos); + float dist = mWorldBounds.distance(cameraPos); // Make sure our scene node is attached if (!mSceneNode->isInSceneGraph()) @@ -386,11 +355,9 @@ void QuadTreeNode::load(const LoadResponseData &data) { assert (!mChunk); - mChunk = new Chunk(mTerrain->getBufferCache().getUVBuffer(), mBounds, data); - mChunk->setVisibilityFlags(mTerrain->getVisiblityFlags()); + mChunk = new Chunk(mTerrain->getBufferCache().getUVBuffer(), mBounds, data.mPositions, data.mNormals, data.mColours); + mChunk->setVisibilityFlags(mTerrain->getVisibilityFlags()); mChunk->setCastShadows(true); - if (!mTerrain->getDistantLandEnabled()) - mChunk->setRenderingDistance(8192); mSceneNode->attachObject(mChunk); mMaterialGenerator->enableShadows(mTerrain->getShadowsEnabled()); @@ -400,13 +367,13 @@ void QuadTreeNode::load(const LoadResponseData &data) { if (mSize == 1) { - mChunk->setMaterial(mMaterialGenerator->generate(mChunk->getMaterial())); + mChunk->setMaterial(mMaterialGenerator->generate()); } else { ensureCompositeMap(); mMaterialGenerator->setCompositeMap(mCompositeMap->getName()); - mChunk->setMaterial(mMaterialGenerator->generateForCompositeMap(mChunk->getMaterial())); + mChunk->setMaterial(mMaterialGenerator->generateForCompositeMap()); } } // else: will be loaded in loadMaterials() after background thread has finished loading layers @@ -532,13 +499,13 @@ void QuadTreeNode::loadMaterials() { if (mSize == 1) { - mChunk->setMaterial(mMaterialGenerator->generate(mChunk->getMaterial())); + mChunk->setMaterial(mMaterialGenerator->generate()); } else { ensureCompositeMap(); mMaterialGenerator->setCompositeMap(mCompositeMap->getName()); - mChunk->setMaterial(mMaterialGenerator->generateForCompositeMap(mChunk->getMaterial())); + mChunk->setMaterial(mMaterialGenerator->generateForCompositeMap()); } } } @@ -550,11 +517,12 @@ void QuadTreeNode::prepareForCompositeMap(Ogre::TRect area) if (mIsDummy) { // TODO - store this default material somewhere instead of creating one for each empty cell - MaterialGenerator matGen(mTerrain->getShadersEnabled()); + MaterialGenerator matGen; + matGen.enableShaders(mTerrain->getShadersEnabled()); std::vector layer; layer.push_back(mTerrain->getStorage()->getDefaultLayer()); matGen.setLayerList(layer); - makeQuad(sceneMgr, area.left, area.top, area.right, area.bottom, matGen.generateForCompositeMapRTT(Ogre::MaterialPtr())); + makeQuad(sceneMgr, area.left, area.top, area.right, area.bottom, matGen.generateForCompositeMapRTT()); return; } if (mSize > 1) @@ -577,7 +545,7 @@ void QuadTreeNode::prepareForCompositeMap(Ogre::TRect area) else { // TODO: when to destroy? - Ogre::MaterialPtr material = mMaterialGenerator->generateForCompositeMapRTT(Ogre::MaterialPtr()); + Ogre::MaterialPtr material = mMaterialGenerator->generateForCompositeMapRTT(); makeQuad(sceneMgr, area.left, area.top, area.right, area.bottom, material); } } @@ -612,9 +580,9 @@ void QuadTreeNode::applyMaterials() mMaterialGenerator->enableShadows(mTerrain->getShadowsEnabled()); mMaterialGenerator->enableSplitShadows(mTerrain->getSplitShadowsEnabled()); if (mSize <= 1) - mChunk->setMaterial(mMaterialGenerator->generate(Ogre::MaterialPtr())); + mChunk->setMaterial(mMaterialGenerator->generate()); else - mChunk->setMaterial(mMaterialGenerator->generateForCompositeMap(Ogre::MaterialPtr())); + mChunk->setMaterial(mMaterialGenerator->generateForCompositeMap()); } if (hasChildren()) for (int i=0; i<4; ++i) diff --git a/components/terrain/quadtreenode.hpp b/components/terrain/quadtreenode.hpp index c57589487..626572701 100644 --- a/components/terrain/quadtreenode.hpp +++ b/components/terrain/quadtreenode.hpp @@ -14,7 +14,7 @@ namespace Ogre namespace Terrain { - class World; + class DefaultWorld; class Chunk; class MaterialGenerator; struct LoadResponseData; @@ -48,7 +48,7 @@ namespace Terrain /// @param size size (in *cell* units!) /// @param center center (in *cell* units!) /// @param parent parent node - QuadTreeNode (World* terrain, ChildDirection dir, float size, const Ogre::Vector2& center, QuadTreeNode* parent); + QuadTreeNode (DefaultWorld* terrain, ChildDirection dir, float size, const Ogre::Vector2& center, QuadTreeNode* parent); ~QuadTreeNode(); /// Rebuild all materials @@ -95,7 +95,7 @@ namespace Terrain const Ogre::AxisAlignedBox& getWorldBoundingBox(); - World* getTerrain() { return mTerrain; } + DefaultWorld* getTerrain() { return mTerrain; } /// Adjust LODs for the given camera position, possibly splitting up chunks or merging them. /// @param force Always choose to render this node, even if not the perfect LOD. @@ -158,7 +158,7 @@ namespace Terrain Chunk* mChunk; - World* mTerrain; + DefaultWorld* mTerrain; Ogre::TexturePtr mCompositeMap; diff --git a/components/terrain/terraingrid.cpp b/components/terrain/terraingrid.cpp new file mode 100644 index 000000000..6a66ac3d7 --- /dev/null +++ b/components/terrain/terraingrid.cpp @@ -0,0 +1,164 @@ +#include "terraingrid.hpp" + +#include +#include +#include + +#include "chunk.hpp" + +namespace Terrain +{ + +TerrainGrid::TerrainGrid(Ogre::SceneManager *sceneMgr, Terrain::Storage *storage, int visibilityFlags, bool shaders, Terrain::Alignment align) + : Terrain::World(sceneMgr, storage, visibilityFlags, shaders, align) + , mVisible(true) +{ + mRootNode = mSceneMgr->getRootSceneNode()->createChildSceneNode(); +} + +TerrainGrid::~TerrainGrid() +{ + while (!mGrid.empty()) + { + unloadCell(mGrid.begin()->first.first, mGrid.begin()->first.second); + } + + mSceneMgr->destroySceneNode(mRootNode); +} + +void TerrainGrid::update(const Ogre::Vector3 &cameraPos) +{ +} + +void TerrainGrid::loadCell(int x, int y) +{ + if (mGrid.find(std::make_pair(x, y)) != mGrid.end()) + return; // already loaded + + Ogre::Vector2 center(x+0.5, y+0.5); + float minH, maxH; + if (!mStorage->getMinMaxHeights(1, center, minH, maxH)) + return; // no terrain defined + + Ogre::Vector3 min (-0.5*mStorage->getCellWorldSize(), + -0.5*mStorage->getCellWorldSize(), + minH); + Ogre::Vector3 max (0.5*mStorage->getCellWorldSize(), + 0.5*mStorage->getCellWorldSize(), + maxH); + + Ogre::AxisAlignedBox bounds(min, max); + + GridElement element; + + Ogre::Vector2 worldCenter = center*mStorage->getCellWorldSize(); + element.mSceneNode = mRootNode->createChildSceneNode(Ogre::Vector3(worldCenter.x, worldCenter.y, 0)); + + std::vector positions; + std::vector normals; + std::vector colours; + mStorage->fillVertexBuffers(0, 1, center, mAlign, positions, normals, colours); + + element.mChunk = new Terrain::Chunk(mCache.getUVBuffer(), bounds, positions, normals, colours); + element.mChunk->setIndexBuffer(mCache.getIndexBuffer(0)); + element.mChunk->setVisibilityFlags(mVisibilityFlags); + element.mChunk->setCastShadows(true); + + std::vector blendmaps; + std::vector layerList; + mStorage->getBlendmaps(1, center, mShaders, blendmaps, layerList); + + element.mMaterialGenerator.setLayerList(layerList); + + // upload blendmaps to GPU + std::vector blendTextures; + for (std::vector::const_iterator it = blendmaps.begin(); it != blendmaps.end(); ++it) + { + static int count=0; + Ogre::TexturePtr map = Ogre::TextureManager::getSingleton().createManual("terrain/blend/" + + Ogre::StringConverter::toString(count++), Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME, + Ogre::TEX_TYPE_2D, it->getWidth(), it->getHeight(), 0, it->format); + + Ogre::DataStreamPtr stream(new Ogre::MemoryDataStream(it->data, it->getWidth()*it->getHeight()*Ogre::PixelUtil::getNumElemBytes(it->format), true)); + map->loadRawData(stream, it->getWidth(), it->getHeight(), it->format); + blendTextures.push_back(map); + } + + element.mMaterialGenerator.setBlendmapList(blendTextures); + + element.mSceneNode->attachObject(element.mChunk); + updateMaterial(element); + + mGrid[std::make_pair(x,y)] = element; +} + +void TerrainGrid::updateMaterial(GridElement &element) +{ + element.mMaterialGenerator.enableShadows(getShadowsEnabled()); + element.mMaterialGenerator.enableSplitShadows(getSplitShadowsEnabled()); + element.mChunk->setMaterial(element.mMaterialGenerator.generate()); +} + +void TerrainGrid::unloadCell(int x, int y) +{ + Grid::iterator it = mGrid.find(std::make_pair(x,y)); + if (it == mGrid.end()) + return; + + GridElement& element = it->second; + delete element.mChunk; + element.mChunk = NULL; + + const std::vector& blendmaps = element.mMaterialGenerator.getBlendmapList(); + for (std::vector::const_iterator it = blendmaps.begin(); it != blendmaps.end(); ++it) + Ogre::TextureManager::getSingleton().remove((*it)->getName()); + + mSceneMgr->destroySceneNode(element.mSceneNode); + element.mSceneNode = NULL; + + mGrid.erase(it); +} + +void TerrainGrid::applyMaterials(bool shadows, bool splitShadows) +{ + mShadows = shadows; + mSplitShadows = splitShadows; + for (Grid::iterator it = mGrid.begin(); it != mGrid.end(); ++it) + { + updateMaterial(it->second); + } +} + +bool TerrainGrid::getVisible() +{ + return mVisible; +} + +void TerrainGrid::setVisible(bool visible) +{ + mVisible = visible; + mRootNode->setVisible(visible); +} + +Ogre::AxisAlignedBox TerrainGrid::getWorldBoundingBox (const Ogre::Vector2& center) +{ + int cellX = std::floor(center.x); + int cellY = std::floor(center.y); + + Grid::iterator it = mGrid.find(std::make_pair(cellX, cellY)); + if (it == mGrid.end()) + return Ogre::AxisAlignedBox::BOX_NULL; + + Terrain::Chunk* chunk = it->second.mChunk; + Ogre::SceneNode* node = it->second.mSceneNode; + Ogre::AxisAlignedBox box = chunk->getBoundingBox(); + box = Ogre::AxisAlignedBox(box.getMinimum() + node->getPosition(), box.getMaximum() + node->getPosition()); + return box; +} + +void TerrainGrid::syncLoad() +{ + +} + +} diff --git a/components/terrain/terraingrid.hpp b/components/terrain/terraingrid.hpp new file mode 100644 index 000000000..7cbf45576 --- /dev/null +++ b/components/terrain/terraingrid.hpp @@ -0,0 +1,71 @@ +#ifndef COMPONENTS_TERRAIN_TERRAINGRID_H +#define COMPONENTS_TERRAIN_TERRAINGRID_H + +#include "world.hpp" +#include "material.hpp" + +namespace Terrain +{ + class Chunk; + + struct GridElement + { + Ogre::SceneNode* mSceneNode; + + Terrain::MaterialGenerator mMaterialGenerator; + + Terrain::Chunk* mChunk; + }; + + /// @brief Simple terrain implementation that loads cells in a grid, with no LOD + class TerrainGrid : public Terrain::World + { + public: + /// @note takes ownership of \a storage + /// @param sceneMgr scene manager to use + /// @param storage Storage instance to get terrain data from (heights, normals, colors, textures..) + /// @param visbilityFlags visibility flags for the created meshes + /// @param shaders Whether to use splatting shader, or multi-pass fixed function splatting. Shader is usually + /// faster so this is just here for compatibility. + /// @param align The align of the terrain, see Alignment enum + TerrainGrid(Ogre::SceneManager* sceneMgr, + Terrain::Storage* storage, int visibilityFlags, bool shaders, Terrain::Alignment align); + ~TerrainGrid(); + + /// Update chunk LODs according to this camera position + virtual void update (const Ogre::Vector3& cameraPos); + + virtual void loadCell(int x, int y); + virtual void unloadCell(int x, int y); + + /// Get the world bounding box of a chunk of terrain centered at \a center + virtual Ogre::AxisAlignedBox getWorldBoundingBox (const Ogre::Vector2& center); + + /// Show or hide the whole terrain + /// @note this setting may be invalidated once you call Terrain::update, so do not call it while the terrain should be hidden + virtual void setVisible(bool visible); + virtual bool getVisible(); + + /// Recreate materials used by terrain chunks. This should be called whenever settings of + /// the material factory are changed. (Relying on the factory to update those materials is not + /// enough, since turning a feature on/off can change the number of texture units available for layer/blend + /// textures, and to properly respond to this we may need to change the structure of the material, such as + /// adding or removing passes. This can only be achieved by a full rebuild.) + virtual void applyMaterials(bool shadows, bool splitShadows); + + /// Wait until all background loading is complete. + virtual void syncLoad(); + + private: + void updateMaterial (GridElement& element); + + typedef std::map, GridElement> Grid; + Grid mGrid; + + Ogre::SceneNode* mRootNode; + bool mVisible; + }; + +} + +#endif diff --git a/components/terrain/world.cpp b/components/terrain/world.cpp index 3d968470f..93caeb8df 100644 --- a/components/terrain/world.cpp +++ b/components/terrain/world.cpp @@ -1,356 +1,62 @@ #include "world.hpp" #include -#include -#include -#include -#include -#include -#include #include "storage.hpp" -#include "quadtreenode.hpp" -namespace +namespace Terrain { - bool isPowerOfTwo(int x) - { - return ( (x > 0) && ((x & (x - 1)) == 0) ); - } - - int nextPowerOfTwo (int v) - { - if (isPowerOfTwo(v)) return v; - int depth=0; - while(v) - { - v >>= 1; - depth++; - } - return 1 << depth; - } - - Terrain::QuadTreeNode* findNode (const Ogre::Vector2& center, Terrain::QuadTreeNode* node) - { - if (center == node->getCenter()) - return node; - - if (center.x > node->getCenter().x && center.y > node->getCenter().y) - return findNode(center, node->getChild(Terrain::NE)); - else if (center.x > node->getCenter().x && center.y < node->getCenter().y) - return findNode(center, node->getChild(Terrain::SE)); - else if (center.x < node->getCenter().x && center.y > node->getCenter().y) - return findNode(center, node->getChild(Terrain::NW)); - else //if (center.x < node->getCenter().x && center.y < node->getCenter().y) - return findNode(center, node->getChild(Terrain::SW)); - } - +World::World(Ogre::SceneManager* sceneMgr, + Storage* storage, int visibilityFlags, bool shaders, Alignment align) + : mStorage(storage) + , mSceneMgr(sceneMgr) + , mVisibilityFlags(visibilityFlags) + , mShaders(shaders) + , mAlign(align) + , mCache(storage->getCellVertices()) + , mShadows(false) + , mSplitShadows(false) +{ } -namespace Terrain +World::~World() { + delete mStorage; +} - const Ogre::uint REQ_ID_CHUNK = 1; - const Ogre::uint REQ_ID_LAYERS = 2; - - World::World(Ogre::SceneManager* sceneMgr, - Storage* storage, int visibilityFlags, bool distantLand, bool shaders, Alignment align, float minBatchSize, float maxBatchSize) - : mStorage(storage) - , mMinBatchSize(minBatchSize) - , mMaxBatchSize(maxBatchSize) - , mSceneMgr(sceneMgr) - , mVisibilityFlags(visibilityFlags) - , mDistantLand(distantLand) - , mShaders(shaders) - , mVisible(true) - , mAlign(align) - , mMaxX(0) - , mMinX(0) - , mMaxY(0) - , mMinY(0) - , mChunksLoading(0) - , mWorkQueueChannel(0) - , mCache(storage->getCellVertices()) - , mLayerLoadPending(true) - { -#if TERRAIN_USE_SHADER == 0 - if (mShaders) - std::cerr << "Compiled Terrain without shader support, disabling..." << std::endl; - mShaders = false; -#endif - - mCompositeMapSceneMgr = Ogre::Root::getSingleton().createSceneManager(Ogre::ST_GENERIC); - - /// \todo make composite map size configurable - Ogre::Camera* compositeMapCam = mCompositeMapSceneMgr->createCamera("a"); - mCompositeMapRenderTexture = Ogre::TextureManager::getSingleton().createManual( - "terrain/comp/rt", Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME, - Ogre::TEX_TYPE_2D, 128, 128, 0, Ogre::PF_A8B8G8R8, Ogre::TU_RENDERTARGET); - mCompositeMapRenderTarget = mCompositeMapRenderTexture->getBuffer()->getRenderTarget(); - mCompositeMapRenderTarget->setAutoUpdated(false); - mCompositeMapRenderTarget->addViewport(compositeMapCam); - - storage->getBounds(mMinX, mMaxX, mMinY, mMaxY); - - int origSizeX = mMaxX-mMinX; - int origSizeY = mMaxY-mMinY; - - // Dividing a quad tree only works well for powers of two, so round up to the nearest one - int size = nextPowerOfTwo(std::max(origSizeX, origSizeY)); - - // Adjust the center according to the new size - float centerX = (mMinX+mMaxX)/2.f + (size-origSizeX)/2.f; - float centerY = (mMinY+mMaxY)/2.f + (size-origSizeY)/2.f; - - mRootSceneNode = mSceneMgr->getRootSceneNode()->createChildSceneNode(); - - // While building the quadtree, remember leaf nodes since we need to load their layers - LayersRequestData data; - data.mPack = getShadersEnabled(); - - mRootNode = new QuadTreeNode(this, Root, size, Ogre::Vector2(centerX, centerY), NULL); - buildQuadTree(mRootNode, data.mNodes); - //loadingListener->indicateProgress(); - mRootNode->initAabb(); - //loadingListener->indicateProgress(); - mRootNode->initNeighbours(); - //loadingListener->indicateProgress(); - - Ogre::WorkQueue* wq = Ogre::Root::getSingleton().getWorkQueue(); - mWorkQueueChannel = wq->getChannel("LargeTerrain"); - wq->addRequestHandler(mWorkQueueChannel, this); - wq->addResponseHandler(mWorkQueueChannel, this); - - // Start loading layers in the background (for leaf nodes) - wq->addRequest(mWorkQueueChannel, REQ_ID_LAYERS, Ogre::Any(data)); - } - - World::~World() - { - Ogre::WorkQueue* wq = Ogre::Root::getSingleton().getWorkQueue(); - wq->removeRequestHandler(mWorkQueueChannel, this); - wq->removeResponseHandler(mWorkQueueChannel, this); - - delete mRootNode; - delete mStorage; - } - - void World::buildQuadTree(QuadTreeNode *node, std::vector& leafs) - { - float halfSize = node->getSize()/2.f; - - if (node->getSize() <= mMinBatchSize) - { - // We arrived at a leaf - float minZ,maxZ; - Ogre::Vector2 center = node->getCenter(); - float cellWorldSize = getStorage()->getCellWorldSize(); - if (mStorage->getMinMaxHeights(node->getSize(), center, minZ, maxZ)) - { - Ogre::AxisAlignedBox bounds(Ogre::Vector3(-halfSize*cellWorldSize, -halfSize*cellWorldSize, minZ), - Ogre::Vector3(halfSize*cellWorldSize, halfSize*cellWorldSize, maxZ)); - convertBounds(bounds); - node->setBoundingBox(bounds); - leafs.push_back(node); - } - else - node->markAsDummy(); // no data available for this node, skip it - return; - } - - if (node->getCenter().x - halfSize > mMaxX - || node->getCenter().x + halfSize < mMinX - || node->getCenter().y - halfSize > mMaxY - || node->getCenter().y + halfSize < mMinY ) - // Out of bounds of the actual terrain - this will happen because - // we rounded the size up to the next power of two - { - node->markAsDummy(); - return; - } - - // Not a leaf, create its children - node->createChild(SW, halfSize, node->getCenter() - halfSize/2.f); - node->createChild(SE, halfSize, node->getCenter() + Ogre::Vector2(halfSize/2.f, -halfSize/2.f)); - node->createChild(NW, halfSize, node->getCenter() + Ogre::Vector2(-halfSize/2.f, halfSize/2.f)); - node->createChild(NE, halfSize, node->getCenter() + halfSize/2.f); - buildQuadTree(node->getChild(SW), leafs); - buildQuadTree(node->getChild(SE), leafs); - buildQuadTree(node->getChild(NW), leafs); - buildQuadTree(node->getChild(NE), leafs); - - // if all children are dummy, we are also dummy - for (int i=0; i<4; ++i) - { - if (!node->getChild((ChildDirection)i)->isDummy()) - return; - } - node->markAsDummy(); - } - - void World::update(const Ogre::Vector3& cameraPos) - { - if (!mVisible) - return; - mRootNode->update(cameraPos); - mRootNode->updateIndexBuffers(); - } - - Ogre::AxisAlignedBox World::getWorldBoundingBox (const Ogre::Vector2& center) - { - if (center.x > mMaxX - || center.x < mMinX - || center.y > mMaxY - || center.y < mMinY) - return Ogre::AxisAlignedBox::BOX_NULL; - QuadTreeNode* node = findNode(center, mRootNode); - return node->getWorldBoundingBox(); - } - - void World::renderCompositeMap(Ogre::TexturePtr target) - { - mCompositeMapRenderTarget->update(); - target->getBuffer()->blit(mCompositeMapRenderTexture->getBuffer()); - } - - void World::clearCompositeMapSceneManager() - { - mCompositeMapSceneMgr->destroyAllManualObjects(); - mCompositeMapSceneMgr->clearScene(); - } - - float World::getHeightAt(const Ogre::Vector3 &worldPos) - { - return mStorage->getHeightAt(worldPos); - } - - void World::applyMaterials(bool shadows, bool splitShadows) - { - mShadows = shadows; - mSplitShadows = splitShadows; - mRootNode->applyMaterials(); - } - - void World::setVisible(bool visible) - { - if (visible && !mVisible) - mSceneMgr->getRootSceneNode()->addChild(mRootSceneNode); - else if (!visible && mVisible) - mSceneMgr->getRootSceneNode()->removeChild(mRootSceneNode); - - mVisible = visible; - } - - bool World::getVisible() - { - return mVisible; - } - - void World::convertPosition(float &x, float &y, float &z) - { - Terrain::convertPosition(mAlign, x, y, z); - } - - void World::convertPosition(Ogre::Vector3 &pos) - { - convertPosition(pos.x, pos.y, pos.z); - } - - void World::convertBounds(Ogre::AxisAlignedBox& bounds) - { - switch (mAlign) - { - case Align_XY: - return; - case Align_XZ: - convertPosition(bounds.getMinimum()); - convertPosition(bounds.getMaximum()); - // Because we changed sign of Z - std::swap(bounds.getMinimum().z, bounds.getMaximum().z); - return; - case Align_YZ: - convertPosition(bounds.getMinimum()); - convertPosition(bounds.getMaximum()); - return; - } - } - - void World::syncLoad() - { - while (mChunksLoading || mLayerLoadPending) - { - OGRE_THREAD_SLEEP(0); - Ogre::Root::getSingleton().getWorkQueue()->processResponses(); - } - } - - Ogre::WorkQueue::Response* World::handleRequest(const Ogre::WorkQueue::Request *req, const Ogre::WorkQueue *srcQ) - { - if (req->getType() == REQ_ID_CHUNK) - { - const LoadRequestData data = Ogre::any_cast(req->getData()); - - QuadTreeNode* node = data.mNode; - - LoadResponseData* responseData = new LoadResponseData(); - - getStorage()->fillVertexBuffers(node->getNativeLodLevel(), node->getSize(), node->getCenter(), getAlign(), - responseData->mPositions, responseData->mNormals, responseData->mColours); - - return OGRE_NEW Ogre::WorkQueue::Response(req, true, Ogre::Any(responseData)); - } - else // REQ_ID_LAYERS - { - const LayersRequestData data = Ogre::any_cast(req->getData()); - - LayersResponseData* responseData = new LayersResponseData(); - - getStorage()->getBlendmaps(data.mNodes, responseData->mLayerCollections, data.mPack); - - return OGRE_NEW Ogre::WorkQueue::Response(req, true, Ogre::Any(responseData)); - } - } - - void World::handleResponse(const Ogre::WorkQueue::Response *res, const Ogre::WorkQueue *srcQ) - { - assert(res->succeeded() && "Response failure not handled"); - - if (res->getRequest()->getType() == REQ_ID_CHUNK) - { - LoadResponseData* data = Ogre::any_cast(res->getData()); - - const LoadRequestData requestData = Ogre::any_cast(res->getRequest()->getData()); - - requestData.mNode->load(*data); - - delete data; - - --mChunksLoading; - } - else // REQ_ID_LAYERS - { - LayersResponseData* data = Ogre::any_cast(res->getData()); - - for (std::vector::iterator it = data->mLayerCollections.begin(); it != data->mLayerCollections.end(); ++it) - { - it->mTarget->loadLayers(*it); - } +float World::getHeightAt(const Ogre::Vector3 &worldPos) +{ + return mStorage->getHeightAt(worldPos); +} - delete data; +void World::convertPosition(float &x, float &y, float &z) +{ + Terrain::convertPosition(mAlign, x, y, z); +} - mRootNode->loadMaterials(); +void World::convertPosition(Ogre::Vector3 &pos) +{ + convertPosition(pos.x, pos.y, pos.z); +} - mLayerLoadPending = false; - } +void World::convertBounds(Ogre::AxisAlignedBox& bounds) +{ + switch (mAlign) + { + case Align_XY: + return; + case Align_XZ: + convertPosition(bounds.getMinimum()); + convertPosition(bounds.getMaximum()); + // Because we changed sign of Z + std::swap(bounds.getMinimum().z, bounds.getMaximum().z); + return; + case Align_YZ: + convertPosition(bounds.getMinimum()); + convertPosition(bounds.getMaximum()); + return; } +} - void World::queueLoad(QuadTreeNode *node) - { - LoadRequestData data; - data.mNode = node; - - Ogre::Root::getSingleton().getWorkQueue()->addRequest(mWorkQueueChannel, REQ_ID_CHUNK, Ogre::Any(data)); - ++mChunksLoading; - } } diff --git a/components/terrain/world.hpp b/components/terrain/world.hpp index 26a6d034d..beca7903a 100644 --- a/components/terrain/world.hpp +++ b/components/terrain/world.hpp @@ -1,50 +1,38 @@ -#ifndef COMPONENTS_TERRAIN_H -#define COMPONENTS_TERRAIN_H +#ifndef COMPONENTS_TERRAIN_WORLD_H +#define COMPONENTS_TERRAIN_WORLD_H -#include -#include -#include +#include #include "defs.hpp" #include "buffercache.hpp" namespace Ogre { - class Camera; + class SceneManager; } namespace Terrain { - - class QuadTreeNode; class Storage; /** - * @brief A quadtree-based terrain implementation suitable for large data sets. \n - * Near cells are rendered with alpha splatting, distant cells are merged - * together in batches and have their layers pre-rendered onto a composite map. \n - * Cracks at LOD transitions are avoided using stitching. - * @note Multiple cameras are not supported yet + * @brief The basic interface for a terrain world. How the terrain chunks are paged and displayed + * is up to the implementation. */ - class World : public Ogre::WorkQueue::RequestHandler, public Ogre::WorkQueue::ResponseHandler + class World { public: /// @note takes ownership of \a storage /// @param sceneMgr scene manager to use /// @param storage Storage instance to get terrain data from (heights, normals, colors, textures..) /// @param visbilityFlags visibility flags for the created meshes - /// @param distantLand Whether to draw all of the terrain, or only a 3x3 grid around the camera. - /// This is a temporary option until it can be streamlined. /// @param shaders Whether to use splatting shader, or multi-pass fixed function splatting. Shader is usually /// faster so this is just here for compatibility. /// @param align The align of the terrain, see Alignment enum - /// @param minBatchSize Minimum size of a terrain batch along one side (in cell units). Used for building the quad tree. - /// @param maxBatchSize Maximum size of a terrain batch along one side (in cell units). Used when traversing the quad tree. World(Ogre::SceneManager* sceneMgr, - Storage* storage, int visiblityFlags, bool distantLand, bool shaders, Alignment align, float minBatchSize, float maxBatchSize); - ~World(); + Storage* storage, int visiblityFlags, bool shaders, Alignment align); + virtual ~World(); - bool getDistantLandEnabled() { return mDistantLand; } bool getShadersEnabled() { return mShaders; } bool getShadowsEnabled() { return mShadows; } bool getSplitShadowsEnabled() { return mSplitShadows; } @@ -54,138 +42,60 @@ namespace Terrain /// Update chunk LODs according to this camera position /// @note Calling this method might lead to composite textures being rendered, so it is best /// not to call it when render commands are still queued, since that would cause a flush. - void update (const Ogre::Vector3& cameraPos); + virtual void update (const Ogre::Vector3& cameraPos) = 0; + + // This is only a hint and may be ignored by the implementation. + virtual void loadCell(int x, int y) {} + virtual void unloadCell(int x, int y) {} /// Get the world bounding box of a chunk of terrain centered at \a center - Ogre::AxisAlignedBox getWorldBoundingBox (const Ogre::Vector2& center); + virtual Ogre::AxisAlignedBox getWorldBoundingBox (const Ogre::Vector2& center) = 0; Ogre::SceneManager* getSceneManager() { return mSceneMgr; } - Ogre::SceneNode* getRootSceneNode() { return mRootSceneNode; } - Storage* getStorage() { return mStorage; } /// Show or hide the whole terrain - /// @note this setting will be invalidated once you call Terrain::update, so do not call it while the terrain should be hidden - void setVisible(bool visible); - bool getVisible(); + /// @note this setting may be invalidated once you call Terrain::update, so do not call it while the terrain should be hidden + virtual void setVisible(bool visible) = 0; + virtual bool getVisible() = 0; /// Recreate materials used by terrain chunks. This should be called whenever settings of /// the material factory are changed. (Relying on the factory to update those materials is not /// enough, since turning a feature on/off can change the number of texture units available for layer/blend /// textures, and to properly respond to this we may need to change the structure of the material, such as /// adding or removing passes. This can only be achieved by a full rebuild.) - void applyMaterials(bool shadows, bool splitShadows); - - int getVisiblityFlags() { return mVisibilityFlags; } + virtual void applyMaterials(bool shadows, bool splitShadows) = 0; - int getMaxBatchSize() { return mMaxBatchSize; } - - void enableSplattingShader(bool enabled); + int getVisibilityFlags() { return mVisibilityFlags; } Alignment getAlign() { return mAlign; } /// Wait until all background loading is complete. - void syncLoad(); - - private: - // Called from a background worker thread - Ogre::WorkQueue::Response* handleRequest(const Ogre::WorkQueue::Request* req, const Ogre::WorkQueue* srcQ); - // Called from the main thread - void handleResponse(const Ogre::WorkQueue::Response* res, const Ogre::WorkQueue* srcQ); - Ogre::uint16 mWorkQueueChannel; + virtual void syncLoad() {} - bool mDistantLand; + protected: bool mShaders; bool mShadows; bool mSplitShadows; - bool mVisible; Alignment mAlign; - QuadTreeNode* mRootNode; - Ogre::SceneNode* mRootSceneNode; Storage* mStorage; int mVisibilityFlags; - /// The number of chunks currently loading in a background thread. If 0, we have finished loading! - int mChunksLoading; - Ogre::SceneManager* mSceneMgr; - Ogre::SceneManager* mCompositeMapSceneMgr; - - /// Bounds in cell units - float mMinX, mMaxX, mMinY, mMaxY; - - /// Minimum size of a terrain batch along one side (in cell units) - float mMinBatchSize; - /// Maximum size of a terrain batch along one side (in cell units) - float mMaxBatchSize; - - void buildQuadTree(QuadTreeNode* node, std::vector& leafs); BufferCache mCache; - // Are layers for leaf nodes loaded? This is done once at startup (but in a background thread) - bool mLayerLoadPending; - public: // ----INTERNAL---- - Ogre::SceneManager* getCompositeMapSceneManager() { return mCompositeMapSceneMgr; } BufferCache& getBufferCache() { return mCache; } - bool areLayersLoaded() { return !mLayerLoadPending; } - - // Delete all quads - void clearCompositeMapSceneManager(); - void renderCompositeMap (Ogre::TexturePtr target); - // Convert the given position from Z-up align, i.e. Align_XY to the wanted align set in mAlign void convertPosition (float& x, float& y, float& z); void convertPosition (Ogre::Vector3& pos); void convertBounds (Ogre::AxisAlignedBox& bounds); - - // Adds a WorkQueue request to load a chunk for this node in the background. - void queueLoad (QuadTreeNode* node); - - private: - Ogre::RenderTarget* mCompositeMapRenderTarget; - Ogre::TexturePtr mCompositeMapRenderTexture; - }; - - struct LoadRequestData - { - QuadTreeNode* mNode; - - friend std::ostream& operator<<(std::ostream& o, const LoadRequestData& r) - { return o; } - }; - - struct LoadResponseData - { - std::vector mPositions; - std::vector mNormals; - std::vector mColours; - - friend std::ostream& operator<<(std::ostream& o, const LoadResponseData& r) - { return o; } - }; - - struct LayersRequestData - { - std::vector mNodes; - bool mPack; - - friend std::ostream& operator<<(std::ostream& o, const LayersRequestData& r) - { return o; } - }; - - struct LayersResponseData - { - std::vector mLayerCollections; - - friend std::ostream& operator<<(std::ostream& o, const LayersResponseData& r) - { return o; } }; } diff --git a/components/to_utf8/to_utf8.cpp b/components/to_utf8/to_utf8.cpp index 59a9aff80..c53cf62b5 100644 --- a/components/to_utf8/to_utf8.cpp +++ b/components/to_utf8/to_utf8.cpp @@ -4,6 +4,7 @@ #include #include #include +#include /* This file contains the code to translate from WINDOWS-1252 (native charset used in English version of Morrowind) to UTF-8. The library @@ -329,8 +330,10 @@ ToUTF8::FromType ToUTF8::calculateEncoding(const std::string& encodingName) return ToUTF8::WINDOWS_1250; else if (encodingName == "win1251") return ToUTF8::WINDOWS_1251; - else + else if (encodingName == "win1252") return ToUTF8::WINDOWS_1252; + else + throw std::runtime_error(std::string("Unknown encoding '") + encodingName + std::string("', see openmw --help for available options.")); } std::string ToUTF8::encodingUsingMessage(const std::string& encodingName) @@ -339,6 +342,8 @@ std::string ToUTF8::encodingUsingMessage(const std::string& encodingName) return "Using Central and Eastern European font encoding."; else if (encodingName == "win1251") return "Using Cyrillic font encoding."; - else + else if (encodingName == "win1252") return "Using default (English) font encoding."; + else + throw std::runtime_error(std::string("Unknown encoding '") + encodingName + std::string("', see openmw --help for available options.")); } diff --git a/components/translation/translation.cpp b/components/translation/translation.cpp index 423c3971a..51947f6f9 100644 --- a/components/translation/translation.cpp +++ b/components/translation/translation.cpp @@ -6,6 +6,11 @@ namespace Translation { + Storage::Storage() + : mEncoder(NULL) + { + } + void Storage::loadTranslationData(const Files::Collections& dataFileCollections, const std::string& esmFileName) { @@ -42,7 +47,7 @@ namespace Translation void Storage::loadDataFromStream(ContainerType& container, std::istream& stream) { std::string line; - while (!stream.eof()) + while (!stream.eof() && !stream.fail()) { std::getline( stream, line ); if (!line.empty() && *line.rbegin() == '\r') diff --git a/components/translation/translation.hpp b/components/translation/translation.hpp index bca9ea255..6a3f84ba1 100644 --- a/components/translation/translation.hpp +++ b/components/translation/translation.hpp @@ -9,6 +9,7 @@ namespace Translation class Storage { public: + Storage(); void loadTranslationData(const Files::Collections& dataFileCollections, const std::string& esmFileName); diff --git a/components/widgets/box.cpp b/components/widgets/box.cpp new file mode 100644 index 000000000..0ac2ff7fd --- /dev/null +++ b/components/widgets/box.cpp @@ -0,0 +1,425 @@ +#include "box.hpp" + +namespace Gui +{ + + void AutoSizedWidget::notifySizeChange (MyGUI::Widget* w) + { + MyGUI::Widget * parent = w->getParent(); + if (parent != 0) + { + if (mExpandDirection.isLeft()) + { + int hdiff = getRequestedSize ().width - w->getSize().width; + w->setPosition(w->getPosition() - MyGUI::IntPoint(hdiff, 0)); + } + w->setSize(getRequestedSize ()); + + while (parent != 0) + { + Box * b = dynamic_cast(parent); + if (b) + b->notifyChildrenSizeChanged(); + else + break; + parent = parent->getParent(); + } + } + } + + + MyGUI::IntSize AutoSizedTextBox::getRequestedSize() + { + return getTextSize(); + } + + void AutoSizedTextBox::setCaption(const MyGUI::UString& _value) + { + TextBox::setCaption(_value); + + notifySizeChange (this); + } + + void AutoSizedTextBox::setPropertyOverride(const std::string& _key, const std::string& _value) + { + if (_key == "ExpandDirection") + { + mExpandDirection = MyGUI::Align::parse (_value); + } + else + { + TextBox::setPropertyOverride (_key, _value); + } + } + + MyGUI::IntSize AutoSizedEditBox::getRequestedSize() + { + if (getAlign().isHStretch()) + throw std::runtime_error("AutoSizedEditBox can't have HStretch align (" + getName() + ")"); + return MyGUI::IntSize(getSize().width, getTextSize().height); + } + + void AutoSizedEditBox::setCaption(const MyGUI::UString& _value) + { + EditBox::setCaption(_value); + + notifySizeChange (this); + } + + void AutoSizedEditBox::setPropertyOverride(const std::string& _key, const std::string& _value) + { + if (_key == "ExpandDirection") + { + mExpandDirection = MyGUI::Align::parse (_value); + } + else + { + EditBox::setPropertyOverride (_key, _value); + } + } + + + MyGUI::IntSize AutoSizedButton::getRequestedSize() + { + MyGUI::IntSize padding(24, 8); + if (isUserString("TextPadding")) + padding = MyGUI::IntSize::parse(getUserString("TextPadding")); + + MyGUI::IntSize size = getTextSize() + MyGUI::IntSize(padding.width,padding.height); + return size; + } + + void AutoSizedButton::setCaption(const MyGUI::UString& _value) + { + Button::setCaption(_value); + + notifySizeChange (this); + } + + void AutoSizedButton::setPropertyOverride(const std::string& _key, const std::string& _value) + { + if (_key == "ExpandDirection") + { + mExpandDirection = MyGUI::Align::parse (_value); + } + else + { + Button::setPropertyOverride (_key, _value); + } + } + + Box::Box() + : mSpacing(4) + , mPadding(0) + , mAutoResize(false) + { + + } + + void Box::notifyChildrenSizeChanged () + { + align(); + } + + bool Box::_setPropertyImpl(const std::string& _key, const std::string& _value) + { + if (_key == "Spacing") + mSpacing = MyGUI::utility::parseValue(_value); + else if (_key == "Padding") + mPadding = MyGUI::utility::parseValue(_value); + else if (_key == "AutoResize") + mAutoResize = MyGUI::utility::parseValue(_value); + else + return false; + + return true; + } + + void HBox::align () + { + unsigned int count = getChildCount (); + size_t h_stretched_count = 0; + int total_width = 0; + int total_height = 0; + std::vector< std::pair > sizes; + sizes.resize(count); + + for (unsigned int i = 0; i < count; ++i) + { + MyGUI::Widget* w = getChildAt(i); + bool hstretch = w->getUserString ("HStretch") == "true"; + bool hidden = w->getUserString("Hidden") == "true"; + if (hidden) + continue; + h_stretched_count += hstretch; + AutoSizedWidget* aw = dynamic_cast(w); + if (aw) + { + sizes[i] = std::make_pair(aw->getRequestedSize (), hstretch); + total_width += aw->getRequestedSize ().width; + total_height = std::max(total_height, aw->getRequestedSize ().height); + } + else + { + sizes[i] = std::make_pair(w->getSize(), hstretch); + total_width += w->getSize().width; + if (!(w->getUserString("VStretch") == "true")) + total_height = std::max(total_height, w->getSize().height); + } + + if (i != count-1) + total_width += mSpacing; + } + + if (mAutoResize && (total_width+mPadding*2 != getSize().width || total_height+mPadding*2 != getSize().height)) + { + setSize(MyGUI::IntSize(total_width+mPadding*2, total_height+mPadding*2)); + return; + } + + + int curX = 0; + for (unsigned int i = 0; i < count; ++i) + { + if (i == 0) + curX += mPadding; + + MyGUI::Widget* w = getChildAt(i); + + bool hidden = w->getUserString("Hidden") == "true"; + if (hidden) + continue; + + bool vstretch = w->getUserString ("VStretch") == "true"; + int max_height = getSize().height - mPadding*2; + int height = vstretch ? max_height : sizes[i].first.height; + + MyGUI::IntCoord widgetCoord; + widgetCoord.left = curX; + widgetCoord.top = mPadding + (getSize().height-mPadding*2 - height) / 2; + + int width = 0; + if (sizes[i].second) + { + if (h_stretched_count == 0) + throw std::logic_error("unexpected"); + width = sizes[i].first.width + (getSize().width-mPadding*2 - total_width)/h_stretched_count; + } + else + width = sizes[i].first.width; + + widgetCoord.width = width; + widgetCoord.height = height; + w->setCoord(widgetCoord); + curX += width; + + if (i != count-1) + curX += mSpacing; + } + } + + void HBox::setPropertyOverride(const std::string& _key, const std::string& _value) + { + if (!Box::_setPropertyImpl (_key, _value)) + MyGUI::Widget::setPropertyOverride(_key, _value); + } + + void HBox::setSize (const MyGUI::IntSize& _value) + { + MyGUI::Widget::setSize (_value); + align(); + } + + void HBox::setCoord (const MyGUI::IntCoord& _value) + { + MyGUI::Widget::setCoord (_value); + align(); + } + + void HBox::onWidgetCreated(MyGUI::Widget* _widget) + { + align(); + } + + MyGUI::IntSize HBox::getRequestedSize () + { + MyGUI::IntSize size(0,0); + for (unsigned int i = 0; i < getChildCount (); ++i) + { + bool hidden = getChildAt(i)->getUserString("Hidden") == "true"; + if (hidden) + continue; + + AutoSizedWidget* w = dynamic_cast(getChildAt(i)); + if (w) + { + MyGUI::IntSize requested = w->getRequestedSize (); + size.height = std::max(size.height, requested.height); + size.width = size.width + requested.width; + if (i != getChildCount()-1) + size.width += mSpacing; + } + else + { + MyGUI::IntSize requested = getChildAt(i)->getSize (); + size.height = std::max(size.height, requested.height); + + if (getChildAt(i)->getUserString("HStretch") != "true") + size.width = size.width + requested.width; + + if (i != getChildCount()-1) + size.width += mSpacing; + } + size.height += mPadding*2; + size.width += mPadding*2; + } + return size; + } + + + + + void VBox::align () + { + unsigned int count = getChildCount (); + size_t v_stretched_count = 0; + int total_height = 0; + int total_width = 0; + std::vector< std::pair > sizes; + sizes.resize(count); + for (unsigned int i = 0; i < count; ++i) + { + MyGUI::Widget* w = getChildAt(i); + + bool hidden = w->getUserString("Hidden") == "true"; + if (hidden) + continue; + + bool vstretch = w->getUserString ("VStretch") == "true"; + v_stretched_count += vstretch; + AutoSizedWidget* aw = dynamic_cast(w); + if (aw) + { + sizes[i] = std::make_pair(aw->getRequestedSize (), vstretch); + total_height += aw->getRequestedSize ().height; + total_width = std::max(total_width, aw->getRequestedSize ().width); + } + else + { + sizes[i] = std::make_pair(w->getSize(), vstretch); + total_height += w->getSize().height; + + if (!(w->getUserString("HStretch") == "true")) + total_width = std::max(total_width, w->getSize().width); + } + + if (i != count-1) + total_height += mSpacing; + } + + if (mAutoResize && (total_width+mPadding*2 != getSize().width || total_height+mPadding*2 != getSize().height)) + { + setSize(MyGUI::IntSize(total_width+mPadding*2, total_height+mPadding*2)); + return; + } + + + int curY = 0; + for (unsigned int i = 0; i < count; ++i) + { + if (i==0) + curY += mPadding; + + MyGUI::Widget* w = getChildAt(i); + + bool hidden = w->getUserString("Hidden") == "true"; + if (hidden) + continue; + + bool hstretch = w->getUserString ("HStretch") == "true"; + int maxWidth = getSize().width - mPadding*2; + int width = hstretch ? maxWidth : sizes[i].first.width; + + MyGUI::IntCoord widgetCoord; + widgetCoord.top = curY; + widgetCoord.left = mPadding + (getSize().width-mPadding*2 - width) / 2; + + int height = 0; + if (sizes[i].second) + { + if (v_stretched_count == 0) + throw std::logic_error("unexpected"); + height = sizes[i].first.height + (getSize().height-mPadding*2 - total_height)/v_stretched_count; + } + else + height = sizes[i].first.height; + + widgetCoord.height = height; + widgetCoord.width = width; + w->setCoord(widgetCoord); + curY += height; + + if (i != count-1) + curY += mSpacing; + } + } + + void VBox::setPropertyOverride(const std::string& _key, const std::string& _value) + { + if (!Box::_setPropertyImpl (_key, _value)) + MyGUI::Widget::setPropertyOverride(_key, _value); + } + + void VBox::setSize (const MyGUI::IntSize& _value) + { + MyGUI::Widget::setSize (_value); + align(); + } + + void VBox::setCoord (const MyGUI::IntCoord& _value) + { + MyGUI::Widget::setCoord (_value); + align(); + } + + MyGUI::IntSize VBox::getRequestedSize () + { + MyGUI::IntSize size(0,0); + for (unsigned int i = 0; i < getChildCount (); ++i) + { + bool hidden = getChildAt(i)->getUserString("Hidden") == "true"; + if (hidden) + continue; + + AutoSizedWidget* w = dynamic_cast(getChildAt(i)); + if (w) + { + MyGUI::IntSize requested = w->getRequestedSize (); + size.width = std::max(size.width, requested.width); + size.height = size.height + requested.height; + if (i != getChildCount()-1) + size.height += mSpacing; + } + else + { + MyGUI::IntSize requested = getChildAt(i)->getSize (); + size.width = std::max(size.width, requested.width); + + if (getChildAt(i)->getUserString("VStretch") != "true") + size.height = size.height + requested.height; + + if (i != getChildCount()-1) + size.height += mSpacing; + } + size.height += mPadding*2; + size.width += mPadding*2; + } + return size; + } + + void VBox::onWidgetCreated(MyGUI::Widget* _widget) + { + align(); + } + +} diff --git a/components/widgets/box.hpp b/components/widgets/box.hpp new file mode 100644 index 000000000..ccdc5784b --- /dev/null +++ b/components/widgets/box.hpp @@ -0,0 +1,120 @@ +#ifndef OPENMW_WIDGETS_BOX_H +#define OPENMW_WIDGETS_BOX_H + +#include +#include +#include +#include + +namespace Gui +{ + + class AutoSizedWidget + { + public: + AutoSizedWidget() : mExpandDirection(MyGUI::Align::Right) {} + + virtual MyGUI::IntSize getRequestedSize() = 0; + + protected: + void notifySizeChange(MyGUI::Widget* w); + + MyGUI::Align mExpandDirection; + }; + + class AutoSizedTextBox : public AutoSizedWidget, public MyGUI::TextBox + { + MYGUI_RTTI_DERIVED( AutoSizedTextBox ) + + public: + virtual MyGUI::IntSize getRequestedSize(); + virtual void setCaption(const MyGUI::UString& _value); + + protected: + virtual void setPropertyOverride(const std::string& _key, const std::string& _value); + }; + + class AutoSizedEditBox : public AutoSizedWidget, public MyGUI::EditBox + { + MYGUI_RTTI_DERIVED( AutoSizedEditBox ) + + public: + virtual MyGUI::IntSize getRequestedSize(); + virtual void setCaption(const MyGUI::UString& _value); + + protected: + virtual void setPropertyOverride(const std::string& _key, const std::string& _value); + }; + + class AutoSizedButton : public AutoSizedWidget, public MyGUI::Button + { + MYGUI_RTTI_DERIVED( AutoSizedButton ) + + public: + virtual MyGUI::IntSize getRequestedSize(); + virtual void setCaption(const MyGUI::UString& _value); + + protected: + virtual void setPropertyOverride(const std::string& _key, const std::string& _value); + }; + + /** + * @brief A container widget that automatically sizes its children + * @note the box being an AutoSizedWidget as well allows to put boxes inside a box + */ + class Box : public AutoSizedWidget + { + public: + Box(); + + void notifyChildrenSizeChanged(); + + protected: + virtual void align() = 0; + + virtual bool _setPropertyImpl(const std::string& _key, const std::string& _value); + + int mSpacing; // how much space to put between elements + + int mPadding; // outer padding + + bool mAutoResize; // auto resize the box so that it exactly fits all elements + }; + + class HBox : public Box, public MyGUI::Widget + { + MYGUI_RTTI_DERIVED( HBox ) + + public: + virtual void setSize (const MyGUI::IntSize &_value); + virtual void setCoord (const MyGUI::IntCoord &_value); + + protected: + virtual void align(); + virtual MyGUI::IntSize getRequestedSize(); + + virtual void setPropertyOverride(const std::string& _key, const std::string& _value); + + virtual void onWidgetCreated(MyGUI::Widget* _widget); + }; + + class VBox : public Box, public MyGUI::Widget + { + MYGUI_RTTI_DERIVED( VBox) + + public: + virtual void setSize (const MyGUI::IntSize &_value); + virtual void setCoord (const MyGUI::IntCoord &_value); + + protected: + virtual void align(); + virtual MyGUI::IntSize getRequestedSize(); + + virtual void setPropertyOverride(const std::string& _key, const std::string& _value); + + virtual void onWidgetCreated(MyGUI::Widget* _widget); + }; + +} + +#endif diff --git a/apps/openmw/mwgui/imagebutton.cpp b/components/widgets/imagebutton.cpp similarity index 90% rename from apps/openmw/mwgui/imagebutton.cpp rename to components/widgets/imagebutton.cpp index f2565f5c0..1cd882975 100644 --- a/apps/openmw/mwgui/imagebutton.cpp +++ b/components/widgets/imagebutton.cpp @@ -1,8 +1,8 @@ #include "imagebutton.hpp" -#include +#include -namespace MWGui +namespace Gui { void ImageButton::setPropertyOverride(const std::string &_key, const std::string &_value) @@ -44,8 +44,8 @@ namespace MWGui MyGUI::IntSize ImageButton::getRequestedSize(bool logError) { - Ogre::TexturePtr texture = Ogre::TextureManager::getSingleton().getByName(mImageNormal); - if (texture.isNull()) + MyGUI::ITexture* texture = MyGUI::RenderManager::getInstance().getTexture(mImageNormal); + if (!texture) { if (logError) std::cerr << "ImageButton: can't find " << mImageNormal << std::endl; diff --git a/apps/openmw/mwgui/imagebutton.hpp b/components/widgets/imagebutton.hpp similarity index 98% rename from apps/openmw/mwgui/imagebutton.hpp rename to components/widgets/imagebutton.hpp index f4191a3a5..bed6a2794 100644 --- a/apps/openmw/mwgui/imagebutton.hpp +++ b/components/widgets/imagebutton.hpp @@ -3,7 +3,7 @@ #include -namespace MWGui +namespace Gui { /** diff --git a/components/widgets/list.cpp b/components/widgets/list.cpp new file mode 100644 index 000000000..28271e87d --- /dev/null +++ b/components/widgets/list.cpp @@ -0,0 +1,160 @@ +#include "list.hpp" + +#include +#include +#include +#include + +namespace Gui +{ + + MWList::MWList() : + mClient(0) + , mScrollView(0) + , mItemHeight(0) + { + } + + void MWList::initialiseOverride() + { + Base::initialiseOverride(); + + assignWidget(mClient, "Client"); + if (mClient == 0) + mClient = this; + + mScrollView = mClient->createWidgetReal( + "MW_ScrollView", MyGUI::FloatCoord(0.0, 0.0, 1.0, 1.0), + MyGUI::Align::Top | MyGUI::Align::Left | MyGUI::Align::Stretch, getName() + "_ScrollView"); + } + + void MWList::addItem(const std::string& name) + { + mItems.push_back(name); + } + + void MWList::addSeparator() + { + mItems.push_back(""); + } + + void MWList::adjustSize() + { + redraw(); + } + + void MWList::redraw(bool scrollbarShown) + { + const int _scrollBarWidth = 20; // fetch this from skin? + const int scrollBarWidth = scrollbarShown ? _scrollBarWidth : 0; + const int spacing = 3; + size_t viewPosition = -mScrollView->getViewOffset().top; + + while (mScrollView->getChildCount()) + { + MyGUI::Gui::getInstance().destroyWidget(mScrollView->getChildAt(0)); + } + + mItemHeight = 0; + int i=0; + for (std::vector::const_iterator it=mItems.begin(); + it!=mItems.end(); ++it) + { + if (*it != "") + { + if (mListItemSkin.empty()) + return; + MyGUI::Button* button = mScrollView->createWidget( + mListItemSkin, MyGUI::IntCoord(0, mItemHeight, mScrollView->getSize().width - scrollBarWidth - 2, 24), + MyGUI::Align::Left | MyGUI::Align::Top, getName() + "_item_" + (*it)); + button->setCaption((*it)); + button->getSubWidgetText()->setWordWrap(true); + button->getSubWidgetText()->setTextAlign(MyGUI::Align::Left); + button->eventMouseWheel += MyGUI::newDelegate(this, &MWList::onMouseWheel); + button->eventMouseButtonClick += MyGUI::newDelegate(this, &MWList::onItemSelected); + + int height = button->getTextSize().height; + button->setSize(MyGUI::IntSize(button->getSize().width, height)); + button->setUserData(i); + + mItemHeight += height + spacing; + } + else + { + MyGUI::ImageBox* separator = mScrollView->createWidget("MW_HLine", + MyGUI::IntCoord(2, mItemHeight, mScrollView->getWidth() - scrollBarWidth - 4, 18), + MyGUI::Align::Left | MyGUI::Align::Top | MyGUI::Align::HStretch); + separator->setNeedMouseFocus(false); + + mItemHeight += 18 + spacing; + } + ++i; + } + + // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the scrollbar is hidden + mScrollView->setVisibleVScroll(false); + mScrollView->setCanvasSize(mClient->getSize().width, std::max(mItemHeight, mClient->getSize().height)); + mScrollView->setVisibleVScroll(true); + + if (!scrollbarShown && mItemHeight > mClient->getSize().height) + redraw(true); + + size_t viewRange = mScrollView->getCanvasSize().height; + if(viewPosition > viewRange) + viewPosition = viewRange; + mScrollView->setViewOffset(MyGUI::IntPoint(0, viewPosition * -1)); + } + + void MWList::setPropertyOverride(const std::string &_key, const std::string &_value) + { + if (_key == "ListItemSkin") + mListItemSkin = _value; + else + Base::setPropertyOverride(_key, _value); + } + + unsigned int MWList::getItemCount() + { + return mItems.size(); + } + + std::string MWList::getItemNameAt(unsigned int at) + { + assert(at < mItems.size() && "List item out of bounds"); + return mItems[at]; + } + + void MWList::removeItem(const std::string& name) + { + assert( std::find(mItems.begin(), mItems.end(), name) != mItems.end() ); + mItems.erase( std::find(mItems.begin(), mItems.end(), name) ); + } + + void MWList::clear() + { + mItems.clear(); + } + + void MWList::onMouseWheel(MyGUI::Widget* _sender, int _rel) + { + //NB view offset is negative + if (mScrollView->getViewOffset().top + _rel*0.3 > 0) + mScrollView->setViewOffset(MyGUI::IntPoint(0, 0)); + else + mScrollView->setViewOffset(MyGUI::IntPoint(0, mScrollView->getViewOffset().top + _rel*0.3)); + } + + void MWList::onItemSelected(MyGUI::Widget* _sender) + { + std::string name = _sender->castType()->getCaption(); + int id = *_sender->getUserData(); + eventItemSelected(name, id); + eventWidgetSelected(_sender); + } + + MyGUI::Widget* MWList::getItemWidget(const std::string& name) + { + return mScrollView->findWidget (getName() + "_item_" + name); + } + +} diff --git a/components/widgets/list.hpp b/components/widgets/list.hpp new file mode 100644 index 000000000..093cd8c18 --- /dev/null +++ b/components/widgets/list.hpp @@ -0,0 +1,70 @@ +#ifndef MWGUI_LIST_HPP +#define MWGUI_LIST_HPP + +#include + +namespace Gui +{ + /** + * \brief a very simple list widget that supports word-wrapping entries + * \note if the width or height of the list changes, you must call adjustSize() method + */ + class MWList : public MyGUI::Widget + { + MYGUI_RTTI_DERIVED(MWList) + public: + MWList(); + + typedef MyGUI::delegates::CMultiDelegate2 EventHandle_StringInt; + typedef MyGUI::delegates::CMultiDelegate1 EventHandle_Widget; + + /** + * Event: Item selected with the mouse. + * signature: void method(std::string itemName, int index) + */ + EventHandle_StringInt eventItemSelected; + + /** + * Event: Item selected with the mouse. + * signature: void method(MyGUI::Widget* sender) + */ + EventHandle_Widget eventWidgetSelected; + + + /** + * Call after the size of the list changed, or items were inserted/removed + */ + void adjustSize(); + + void addItem(const std::string& name); + void addSeparator(); ///< add a seperator between the current and the next item. + void removeItem(const std::string& name); + unsigned int getItemCount(); + std::string getItemNameAt(unsigned int at); ///< \attention if there are separators, this method will return "" at the place where the separator is + void clear(); + + MyGUI::Widget* getItemWidget(const std::string& name); + ///< get widget for an item name, useful to set up tooltip + + virtual void setPropertyOverride(const std::string& _key, const std::string& _value); + + protected: + void initialiseOverride(); + + void redraw(bool scrollbarShown = false); + + void onMouseWheel(MyGUI::Widget* _sender, int _rel); + void onItemSelected(MyGUI::Widget* _sender); + + private: + MyGUI::ScrollView* mScrollView; + MyGUI::Widget* mClient; + std::string mListItemSkin; + + std::vector mItems; + + int mItemHeight; // height of all items + }; +} + +#endif diff --git a/components/widgets/numericeditbox.cpp b/components/widgets/numericeditbox.cpp new file mode 100644 index 000000000..5361b3127 --- /dev/null +++ b/components/widgets/numericeditbox.cpp @@ -0,0 +1,74 @@ +#include "numericeditbox.hpp" + +#include + +namespace Gui +{ + + void NumericEditBox::initialiseOverride() + { + Base::initialiseOverride(); + eventEditTextChange += MyGUI::newDelegate(this, &NumericEditBox::onEditTextChange); + + mValue = 0; + setCaption("0"); + } + + void NumericEditBox::shutdownOverride() + { + Base::shutdownOverride(); + eventEditTextChange -= MyGUI::newDelegate(this, &NumericEditBox::onEditTextChange); + } + + void NumericEditBox::onEditTextChange(MyGUI::EditBox *sender) + { + std::string newCaption = sender->getCaption(); + if (newCaption.empty()) + { + return; + } + + try + { + mValue = boost::lexical_cast(newCaption); + int capped = std::min(mMaxValue, std::max(mValue, mMinValue)); + if (capped != mValue) + { + mValue = capped; + setCaption(MyGUI::utility::toString(mValue)); + } + } + catch (boost::bad_lexical_cast&) + { + setCaption(MyGUI::utility::toString(mValue)); + } + + eventValueChanged(mValue); + } + + void NumericEditBox::setValue(int value) + { + if (value != mValue) + { + setCaption(MyGUI::utility::toString(value)); + mValue = value; + } + } + + void NumericEditBox::setMinValue(int minValue) + { + mMinValue = minValue; + } + + void NumericEditBox::setMaxValue(int maxValue) + { + mMaxValue = maxValue; + } + + void NumericEditBox::onKeyLostFocus(MyGUI::Widget* _new) + { + Base::onKeyLostFocus(_new); + setCaption(MyGUI::utility::toString(mValue)); + } + +} diff --git a/components/widgets/numericeditbox.hpp b/components/widgets/numericeditbox.hpp new file mode 100644 index 000000000..20000b3d3 --- /dev/null +++ b/components/widgets/numericeditbox.hpp @@ -0,0 +1,45 @@ +#ifndef OPENMW_NUMERIC_EDIT_BOX_H +#define OPENMW_NUMERIC_EDIT_BOX_H + +#include + +namespace Gui +{ + + /** + * @brief A variant of the EditBox that only allows integer inputs + */ + class NumericEditBox : public MyGUI::EditBox + { + MYGUI_RTTI_DERIVED(NumericEditBox) + + public: + NumericEditBox() + : mValue(0), mMinValue(std::numeric_limits::min()), + mMaxValue(std::numeric_limits::max()) + {} + + void initialiseOverride(); + void shutdownOverride(); + + typedef MyGUI::delegates::CMultiDelegate1 EventHandle_ValueChanged; + EventHandle_ValueChanged eventValueChanged; + + /// @note Does not trigger eventValueChanged + void setValue (int value); + + void setMinValue(int minValue); + void setMaxValue(int maxValue); + private: + void onEditTextChange(MyGUI::EditBox* sender); + void onKeyLostFocus(MyGUI::Widget* _new); + + int mValue; + + int mMinValue; + int mMaxValue; + }; + +} + +#endif diff --git a/components/widgets/tags.cpp b/components/widgets/tags.cpp new file mode 100644 index 000000000..160698c8e --- /dev/null +++ b/components/widgets/tags.cpp @@ -0,0 +1,57 @@ +#include "tags.hpp" + +#include + +namespace Gui +{ + +bool replaceTag(const MyGUI::UString& tag, MyGUI::UString& out, const std::map& fallbackSettings) +{ + std::string fontcolour = "fontcolour="; + size_t fontcolourLength = fontcolour.length(); + + std::string fontcolourhtml = "fontcolourhtml="; + size_t fontcolourhtmlLength = fontcolourhtml.length(); + + if (tag.compare(0, fontcolourLength, fontcolour) == 0) + { + std::string fallbackName = "FontColor_color_" + tag.substr(fontcolourLength); + std::map::const_iterator it = fallbackSettings.find(fallbackName); + if (it == fallbackSettings.end()) + throw std::runtime_error("Unknown fallback name: " + fallbackName); + std::string str = it->second; + + std::string ret[3]; + unsigned int j=0; + for(unsigned int i=0;i::const_iterator it = fallbackSettings.find(fallbackName); + if (it == fallbackSettings.end()) + throw std::runtime_error("Unknown fallback name: " + fallbackName); + std::string str = it->second; + + std::string ret[3]; + unsigned int j=0; + for(unsigned int i=0;i +#include +#include + +namespace Gui +{ + +/// Try to replace a tag. Returns true on success and writes the result to \a out. +bool replaceTag (const MyGUI::UString& tag, MyGUI::UString& out, const std::map& fallbackSettings); + +} + +#endif diff --git a/components/widgets/widgets.cpp b/components/widgets/widgets.cpp new file mode 100644 index 000000000..b35dc88a4 --- /dev/null +++ b/components/widgets/widgets.cpp @@ -0,0 +1,25 @@ +#include "widgets.hpp" + +#include + +#include "list.hpp" +#include "numericeditbox.hpp" +#include "box.hpp" +#include "imagebutton.hpp" + +namespace Gui +{ + + void registerAllWidgets() + { + MyGUI::FactoryManager::getInstance().registerFactory("Widget"); + MyGUI::FactoryManager::getInstance().registerFactory("Widget"); + MyGUI::FactoryManager::getInstance().registerFactory("Widget"); + MyGUI::FactoryManager::getInstance().registerFactory("Widget"); + MyGUI::FactoryManager::getInstance().registerFactory("Widget"); + MyGUI::FactoryManager::getInstance().registerFactory("Widget"); + MyGUI::FactoryManager::getInstance().registerFactory("Widget"); + MyGUI::FactoryManager::getInstance().registerFactory("Widget"); + } + +} diff --git a/components/widgets/widgets.hpp b/components/widgets/widgets.hpp new file mode 100644 index 000000000..d17132135 --- /dev/null +++ b/components/widgets/widgets.hpp @@ -0,0 +1,12 @@ +#ifndef OPENMW_COMPONENTS_WIDGETS_H +#define OPENMW_COMPONENTS_WIDGETS_H + +namespace Gui +{ + + /// Register all widgets from this component with MyGUI's factory manager. + void registerAllWidgets(); + +} + +#endif diff --git a/credits.txt b/credits.txt index da5417034..c6a5c7169 100644 --- a/credits.txt +++ b/credits.txt @@ -19,8 +19,8 @@ Alexander Olofsson (Ace) Artem Kotsynyak (greye) Arthur Moore (EmperorArthur) athile +Bret Curtis (psi29a) Britt Mathis (galdor557) -BrotherBrick cc9cii Chris Boyce (slothlife) Chris Robinson (KittyCat) @@ -30,21 +30,30 @@ darkf Dmitry Shkurskiy (endorph) Douglas Diniz (Dgdiniz) Douglas Mencken (dougmencken) +dreamer-dead Edmondo Tommasina (edmondo) Eduard Cot (trombonecot) Eli2 Emanuel GuĆ©vel (potatoesmaster) +eroen Fil Krynicki (filkry) +GaÅ”per Sedej gugus/gus +Hallfaer Tuilinn +Internecine Jacob Essex (Yacoby) Jannik Heller (scrawl) Jason Hooks (jhooks) +jeaye Jeffrey Haines (Jyby) +Jengerer Joel Graff (graffy) John Blomberg (fstp) +Jordan Ayers Jordan Milne Julien Voisin (jvoisin/ap0) Karl-Felix Glatzer (k1ll) +Kevin Poitra (PuppyKevin) Lars Sƶderberg (Lazaroth) lazydev Leon Saunders (emoose) @@ -53,32 +62,46 @@ Manuel Edelmann (vorenon) Marc Bouvier (CramitDeFrog) Marcin Hulist (Gohan) Mark Siewert (mark76) +Marco Melletti (mellotanica) Mateusz Kołaczek (PL_kolek) megaton Michael Hogan (Xethik) Michael Mc Donnell Michael Papageorgiou (werdanith) Michał Bień (Glorf) +Miroslav Puda (pakanek) +MiroslavR Nathan Jeffords (blunted2night) Nikolay Kasyanov (corristo) +nobrakal Nolan Poe (nopoe) Paul McElroy (Greendogo) Pieter van der Kloet (pvdk) Radu-Marius Popovici (rpopovici) +riothamus +Robert MacGregor (Ragora) +Rohit Nirmal Roman Melnik (Kromgart) Roman Proskuryakov (humbug) +sandstranger Sandy Carter (bwrsandman) +Scott Howard Sebastian Wick (swick) Sergey Shambir sir_herrbatka +Stefan Galowicz (bogglez) +Stanislav Bobrov (Jiub) Sylvain Thesnieres (Garvek) +terrorfisch Thomas Luppi (Digmaster) Tom Mason (wheybags) Torben Leif Carrington (TorbenC) +Vincent Heuken +vocollapse Packagers: Alexander Olofsson (Ace) - Windows -BrotherBrick - Ubuntu Linux +Bret Curtis (psi29a) - Ubuntu Linux Edmondo Tommasina (edmondo) - Gentoo Linux Julian Ospald (hasufell) - Gentoo Linux Karl-Felix Glatzer (k1ll) - Linux Binaries @@ -86,25 +109,25 @@ Kenny Armstrong (artorius) - Fedora Linux Nikolay Kasyanov (corristo) - Mac OS X Sandy Carter (bwrsandman) - Arch Linux - Public Relations and Translations: Alex McKibben (WeirdSexy) - Podcaster Artem Kotsynyak (greye) - Russian News Writer Jim Clauwaert (Zedd) - Public Outreach Julien Voisin (jvoisin/ap0) - French News Writer +Tom Koenderink (Okulo) - English News Writer Lukasz Gromanowski (lgro) - English News Writer Mickey Lyle (raevol) - Release Manager Pithorn - Chinese News Writer sir_herrbatka - Polish News Writer - +Dawid Lakomy (Vedyimyn) - Polish News Writer Website: -Lukasz Gromanowski (lgro) - Website Administrator +Lukasz Gromanowski (Lgro) - Website Administrator Ryan Sardonic (Wry) - Wiki Editor sir_herrbatka - Forum Administrator - Formula Research: +Hrnchamd Epsilon fragonard Greendogo @@ -114,15 +137,15 @@ Myckel natirips Sadler - Artwork: Necrod - OpenMW Logo Mickey Lyle (raevol) - Wordpress Theme -Okulo - OpenMW Editor Icons +Tom Koenderink (Okulo), SirHerrbatka, crysthala, Shnatsel - OpenMW Editor Icons Inactive Contributors: Ardekantur Armin Preiml +Berulacks Carl Maxwell Diggory Hardy Dmitry Marakasov (AMDmi3) @@ -136,6 +159,7 @@ Kingpix Lordrea Michal Sciubidlo Nicolay Korslund +Nekochan pchan3 penguinroad psi29a diff --git a/docs/Doxyfile.cmake b/docs/Doxyfile.cmake new file mode 100644 index 000000000..38ad84165 --- /dev/null +++ b/docs/Doxyfile.cmake @@ -0,0 +1,2354 @@ +# Doxyfile 1.8.7 + +# This file describes the settings to be used by the documentation system +# doxygen (www.doxygen.org) for a project. +# +# All text after a double hash (##) is considered a comment and is placed in +# front of the TAG it is preceding. +# +# All text after a single hash (#) is considered a comment and will be ignored. +# The format is: +# TAG = value [value, ...] +# For lists, items can also be appended using: +# TAG += value [value, ...] +# Values that contain spaces should be placed between quotes (\" \"). + +#--------------------------------------------------------------------------- +# Project related configuration options +#--------------------------------------------------------------------------- + +# This tag specifies the encoding used for all characters in the config file +# that follow. The default is UTF-8 which is also the encoding used for all text +# before the first occurrence of this tag. Doxygen uses libiconv (or the iconv +# built into libc) for the transcoding. See http://www.gnu.org/software/libiconv +# for the list of possible encodings. +# The default value is: UTF-8. + +DOXYFILE_ENCODING = UTF-8 + +# The PROJECT_NAME tag is a single word (or a sequence of words surrounded by +# double-quotes, unless you are using Doxywizard) that should identify the +# project for which the documentation is generated. This name is used in the +# title of most generated pages and in a few other places. +# The default value is: My Project. + +PROJECT_NAME = OpenMW + +# The PROJECT_NUMBER tag can be used to enter a project or revision number. This +# could be handy for archiving the generated documentation or if some version +# control system is used. + +PROJECT_NUMBER = + +# Using the PROJECT_BRIEF tag one can provide an optional one line description +# for a project that appears at the top of each page and should give viewer a +# quick idea about the purpose of the project. Keep the description short. + +PROJECT_BRIEF = + +# With the PROJECT_LOGO tag one can specify an logo or icon that is included in +# the documentation. The maximum height of the logo should not exceed 55 pixels +# and the maximum width should not exceed 200 pixels. Doxygen will copy the logo +# to the output directory. + +PROJECT_LOGO = + +# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) path +# into which the generated documentation will be written. If a relative path is +# entered, it will be relative to the location where doxygen was started. If +# left blank the current directory will be used. + +OUTPUT_DIRECTORY = @OpenMW_BINARY_DIR@/docs/Doxygen + +# If the CREATE_SUBDIRS tag is set to YES, then doxygen will create 4096 sub- +# directories (in 2 levels) under the output directory of each output format and +# will distribute the generated files over these directories. Enabling this +# option can be useful when feeding doxygen a huge amount of source files, where +# putting all generated files in the same directory would otherwise causes +# performance problems for the file system. +# The default value is: NO. + +CREATE_SUBDIRS = NO + +# If the ALLOW_UNICODE_NAMES tag is set to YES, doxygen will allow non-ASCII +# characters to appear in the names of generated files. If set to NO, non-ASCII +# characters will be escaped, for example _xE3_x81_x84 will be used for Unicode +# U+3044. +# The default value is: NO. + +ALLOW_UNICODE_NAMES = NO + +# The OUTPUT_LANGUAGE tag is used to specify the language in which all +# documentation generated by doxygen is written. Doxygen will use this +# information to generate all constant output in the proper language. +# Possible values are: Afrikaans, Arabic, Armenian, Brazilian, Catalan, Chinese, +# Chinese-Traditional, Croatian, Czech, Danish, Dutch, English (United States), +# Esperanto, Farsi (Persian), Finnish, French, German, Greek, Hungarian, +# Indonesian, Italian, Japanese, Japanese-en (Japanese with English messages), +# Korean, Korean-en (Korean with English messages), Latvian, Lithuanian, +# Macedonian, Norwegian, Persian (Farsi), Polish, Portuguese, Romanian, Russian, +# Serbian, Serbian-Cyrillic, Slovak, Slovene, Spanish, Swedish, Turkish, +# Ukrainian and Vietnamese. +# The default value is: English. + +OUTPUT_LANGUAGE = English + +# If the BRIEF_MEMBER_DESC tag is set to YES doxygen will include brief member +# descriptions after the members that are listed in the file and class +# documentation (similar to Javadoc). Set to NO to disable this. +# The default value is: YES. + +BRIEF_MEMBER_DESC = YES + +# If the REPEAT_BRIEF tag is set to YES doxygen will prepend the brief +# description of a member or function before the detailed description +# +# Note: If both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the +# brief descriptions will be completely suppressed. +# The default value is: YES. + +REPEAT_BRIEF = YES + +# This tag implements a quasi-intelligent brief description abbreviator that is +# used to form the text in various listings. Each string in this list, if found +# as the leading text of the brief description, will be stripped from the text +# and the result, after processing the whole list, is used as the annotated +# text. Otherwise, the brief description is used as-is. If left blank, the +# following values are used ($name is automatically replaced with the name of +# the entity):The $name class, The $name widget, The $name file, is, provides, +# specifies, contains, represents, a, an and the. + +ABBREVIATE_BRIEF = "The $name class" \ + "The $name widget" \ + "The $name file" \ + is \ + provides \ + specifies \ + contains \ + represents \ + a \ + an \ + the + +# If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then +# doxygen will generate a detailed section even if there is only a brief +# description. +# The default value is: NO. + +ALWAYS_DETAILED_SEC = NO + +# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all +# inherited members of a class in the documentation of that class as if those +# members were ordinary class members. Constructors, destructors and assignment +# operators of the base classes will not be shown. +# The default value is: NO. + +INLINE_INHERITED_MEMB = NO + +# If the FULL_PATH_NAMES tag is set to YES doxygen will prepend the full path +# before files name in the file list and in the header files. If set to NO the +# shortest path that makes the file name unique will be used +# The default value is: YES. + +FULL_PATH_NAMES = YES + +# The STRIP_FROM_PATH tag can be used to strip a user-defined part of the path. +# Stripping is only done if one of the specified strings matches the left-hand +# part of the path. The tag can be used to show relative paths in the file list. +# If left blank the directory from which doxygen is run is used as the path to +# strip. +# +# Note that you can specify absolute paths here, but also relative paths, which +# will be relative from the directory where doxygen is started. +# This tag requires that the tag FULL_PATH_NAMES is set to YES. + +STRIP_FROM_PATH = + +# The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of the +# path mentioned in the documentation of a class, which tells the reader which +# header file to include in order to use a class. If left blank only the name of +# the header file containing the class definition is used. Otherwise one should +# specify the list of include paths that are normally passed to the compiler +# using the -I flag. + +STRIP_FROM_INC_PATH = + +# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter (but +# less readable) file names. This can be useful is your file systems doesn't +# support long names like on DOS, Mac, or CD-ROM. +# The default value is: NO. + +SHORT_NAMES = NO + +# If the JAVADOC_AUTOBRIEF tag is set to YES then doxygen will interpret the +# first line (until the first dot) of a Javadoc-style comment as the brief +# description. If set to NO, the Javadoc-style will behave just like regular Qt- +# style comments (thus requiring an explicit @brief command for a brief +# description.) +# The default value is: NO. + +JAVADOC_AUTOBRIEF = NO + +# If the QT_AUTOBRIEF tag is set to YES then doxygen will interpret the first +# line (until the first dot) of a Qt-style comment as the brief description. If +# set to NO, the Qt-style will behave just like regular Qt-style comments (thus +# requiring an explicit \brief command for a brief description.) +# The default value is: NO. + +QT_AUTOBRIEF = NO + +# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make doxygen treat a +# multi-line C++ special comment block (i.e. a block of //! or /// comments) as +# a brief description. This used to be the default behavior. The new default is +# to treat a multi-line C++ comment block as a detailed description. Set this +# tag to YES if you prefer the old behavior instead. +# +# Note that setting this tag to YES also means that rational rose comments are +# not recognized any more. +# The default value is: NO. + +MULTILINE_CPP_IS_BRIEF = NO + +# If the INHERIT_DOCS tag is set to YES then an undocumented member inherits the +# documentation from any documented member that it re-implements. +# The default value is: YES. + +INHERIT_DOCS = YES + +# If the SEPARATE_MEMBER_PAGES tag is set to YES, then doxygen will produce a +# new page for each member. If set to NO, the documentation of a member will be +# part of the file/class/namespace that contains it. +# The default value is: NO. + +SEPARATE_MEMBER_PAGES = NO + +# The TAB_SIZE tag can be used to set the number of spaces in a tab. Doxygen +# uses this value to replace tabs by spaces in code fragments. +# Minimum value: 1, maximum value: 16, default value: 4. + +TAB_SIZE = 4 + +# This tag can be used to specify a number of aliases that act as commands in +# the documentation. An alias has the form: +# name=value +# For example adding +# "sideeffect=@par Side Effects:\n" +# will allow you to put the command \sideeffect (or @sideeffect) in the +# documentation, which will result in a user-defined paragraph with heading +# "Side Effects:". You can put \n's in the value part of an alias to insert +# newlines. + +ALIASES = + +# This tag can be used to specify a number of word-keyword mappings (TCL only). +# A mapping has the form "name=value". For example adding "class=itcl::class" +# will allow you to use the command class in the itcl::class meaning. + +TCL_SUBST = + +# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources +# only. Doxygen will then generate output that is more tailored for C. For +# instance, some of the names that are used will be different. The list of all +# members will be omitted, etc. +# The default value is: NO. + +OPTIMIZE_OUTPUT_FOR_C = NO + +# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java or +# Python sources only. Doxygen will then generate output that is more tailored +# for that language. For instance, namespaces will be presented as packages, +# qualified scopes will look different, etc. +# The default value is: NO. + +OPTIMIZE_OUTPUT_JAVA = NO + +# Set the OPTIMIZE_FOR_FORTRAN tag to YES if your project consists of Fortran +# sources. Doxygen will then generate output that is tailored for Fortran. +# The default value is: NO. + +OPTIMIZE_FOR_FORTRAN = NO + +# Set the OPTIMIZE_OUTPUT_VHDL tag to YES if your project consists of VHDL +# sources. Doxygen will then generate output that is tailored for VHDL. +# The default value is: NO. + +OPTIMIZE_OUTPUT_VHDL = NO + +# Doxygen selects the parser to use depending on the extension of the files it +# parses. With this tag you can assign which parser to use for a given +# extension. Doxygen has a built-in mapping, but you can override or extend it +# using this tag. The format is ext=language, where ext is a file extension, and +# language is one of the parsers supported by doxygen: IDL, Java, Javascript, +# C#, C, C++, D, PHP, Objective-C, Python, Fortran (fixed format Fortran: +# FortranFixed, free formatted Fortran: FortranFree, unknown formatted Fortran: +# Fortran. In the later case the parser tries to guess whether the code is fixed +# or free formatted code, this is the default for Fortran type files), VHDL. For +# instance to make doxygen treat .inc files as Fortran files (default is PHP), +# and .f files as C (default is Fortran), use: inc=Fortran f=C. +# +# Note For files without extension you can use no_extension as a placeholder. +# +# Note that for custom extensions you also need to set FILE_PATTERNS otherwise +# the files are not read by doxygen. + +EXTENSION_MAPPING = + +# If the MARKDOWN_SUPPORT tag is enabled then doxygen pre-processes all comments +# according to the Markdown format, which allows for more readable +# documentation. See http://daringfireball.net/projects/markdown/ for details. +# The output of markdown processing is further processed by doxygen, so you can +# mix doxygen, HTML, and XML commands with Markdown formatting. Disable only in +# case of backward compatibilities issues. +# The default value is: YES. + +MARKDOWN_SUPPORT = YES + +# When enabled doxygen tries to link words that correspond to documented +# classes, or namespaces to their corresponding documentation. Such a link can +# be prevented in individual cases by by putting a % sign in front of the word +# or globally by setting AUTOLINK_SUPPORT to NO. +# The default value is: YES. + +AUTOLINK_SUPPORT = YES + +# If you use STL classes (i.e. std::string, std::vector, etc.) but do not want +# to include (a tag file for) the STL sources as input, then you should set this +# tag to YES in order to let doxygen match functions declarations and +# definitions whose arguments contain STL classes (e.g. func(std::string); +# versus func(std::string) {}). This also make the inheritance and collaboration +# diagrams that involve STL classes more complete and accurate. +# The default value is: NO. + +BUILTIN_STL_SUPPORT = YES + +# If you use Microsoft's C++/CLI language, you should set this option to YES to +# enable parsing support. +# The default value is: NO. + +CPP_CLI_SUPPORT = NO + +# Set the SIP_SUPPORT tag to YES if your project consists of sip (see: +# http://www.riverbankcomputing.co.uk/software/sip/intro) sources only. Doxygen +# will parse them like normal C++ but will assume all classes use public instead +# of private inheritance when no explicit protection keyword is present. +# The default value is: NO. + +SIP_SUPPORT = NO + +# For Microsoft's IDL there are propget and propput attributes to indicate +# getter and setter methods for a property. Setting this option to YES will make +# doxygen to replace the get and set methods by a property in the documentation. +# This will only work if the methods are indeed getting or setting a simple +# type. If this is not the case, or you want to show the methods anyway, you +# should set this option to NO. +# The default value is: YES. + +IDL_PROPERTY_SUPPORT = YES + +# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC +# tag is set to YES, then doxygen will reuse the documentation of the first +# member in the group (if any) for the other members of the group. By default +# all members of a group must be documented explicitly. +# The default value is: NO. + +DISTRIBUTE_GROUP_DOC = NO + +# Set the SUBGROUPING tag to YES to allow class member groups of the same type +# (for instance a group of public functions) to be put as a subgroup of that +# type (e.g. under the Public Functions section). Set it to NO to prevent +# subgrouping. Alternatively, this can be done per class using the +# \nosubgrouping command. +# The default value is: YES. + +SUBGROUPING = YES + +# When the INLINE_GROUPED_CLASSES tag is set to YES, classes, structs and unions +# are shown inside the group in which they are included (e.g. using \ingroup) +# instead of on a separate page (for HTML and Man pages) or section (for LaTeX +# and RTF). +# +# Note that this feature does not work in combination with +# SEPARATE_MEMBER_PAGES. +# The default value is: NO. + +INLINE_GROUPED_CLASSES = NO + +# When the INLINE_SIMPLE_STRUCTS tag is set to YES, structs, classes, and unions +# with only public data fields or simple typedef fields will be shown inline in +# the documentation of the scope in which they are defined (i.e. file, +# namespace, or group documentation), provided this scope is documented. If set +# to NO, structs, classes, and unions are shown on a separate page (for HTML and +# Man pages) or section (for LaTeX and RTF). +# The default value is: NO. + +INLINE_SIMPLE_STRUCTS = NO + +# When TYPEDEF_HIDES_STRUCT tag is enabled, a typedef of a struct, union, or +# enum is documented as struct, union, or enum with the name of the typedef. So +# typedef struct TypeS {} TypeT, will appear in the documentation as a struct +# with name TypeT. When disabled the typedef will appear as a member of a file, +# namespace, or class. And the struct will be named TypeS. This can typically be +# useful for C code in case the coding convention dictates that all compound +# types are typedef'ed and only the typedef is referenced, never the tag name. +# The default value is: NO. + +TYPEDEF_HIDES_STRUCT = NO + +# The size of the symbol lookup cache can be set using LOOKUP_CACHE_SIZE. This +# cache is used to resolve symbols given their name and scope. Since this can be +# an expensive process and often the same symbol appears multiple times in the +# code, doxygen keeps a cache of pre-resolved symbols. If the cache is too small +# doxygen will become slower. If the cache is too large, memory is wasted. The +# cache size is given by this formula: 2^(16+LOOKUP_CACHE_SIZE). The valid range +# is 0..9, the default is 0, corresponding to a cache size of 2^16=65536 +# symbols. At the end of a run doxygen will report the cache usage and suggest +# the optimal cache size from a speed point of view. +# Minimum value: 0, maximum value: 9, default value: 0. + +LOOKUP_CACHE_SIZE = 0 + +#--------------------------------------------------------------------------- +# Build related configuration options +#--------------------------------------------------------------------------- + +# If the EXTRACT_ALL tag is set to YES doxygen will assume all entities in +# documentation are documented, even if no documentation was available. Private +# class members and static file members will be hidden unless the +# EXTRACT_PRIVATE respectively EXTRACT_STATIC tags are set to YES. +# Note: This will also disable the warnings about undocumented members that are +# normally produced when WARNINGS is set to YES. +# The default value is: NO. + +EXTRACT_ALL = YES + +# If the EXTRACT_PRIVATE tag is set to YES all private members of a class will +# be included in the documentation. +# The default value is: NO. + +EXTRACT_PRIVATE = YES + +# If the EXTRACT_PACKAGE tag is set to YES all members with package or internal +# scope will be included in the documentation. +# The default value is: NO. + +EXTRACT_PACKAGE = NO + +# If the EXTRACT_STATIC tag is set to YES all static members of a file will be +# included in the documentation. +# The default value is: NO. + +EXTRACT_STATIC = YES + +# If the EXTRACT_LOCAL_CLASSES tag is set to YES classes (and structs) defined +# locally in source files will be included in the documentation. If set to NO +# only classes defined in header files are included. Does not have any effect +# for Java sources. +# The default value is: YES. + +EXTRACT_LOCAL_CLASSES = YES + +# This flag is only useful for Objective-C code. When set to YES local methods, +# which are defined in the implementation section but not in the interface are +# included in the documentation. If set to NO only methods in the interface are +# included. +# The default value is: NO. + +EXTRACT_LOCAL_METHODS = YES + +# If this flag is set to YES, the members of anonymous namespaces will be +# extracted and appear in the documentation as a namespace called +# 'anonymous_namespace{file}', where file will be replaced with the base name of +# the file that contains the anonymous namespace. By default anonymous namespace +# are hidden. +# The default value is: NO. + +EXTRACT_ANON_NSPACES = YES + +# If the HIDE_UNDOC_MEMBERS tag is set to YES, doxygen will hide all +# undocumented members inside documented classes or files. If set to NO these +# members will be included in the various overviews, but no documentation +# section is generated. This option has no effect if EXTRACT_ALL is enabled. +# The default value is: NO. + +HIDE_UNDOC_MEMBERS = NO + +# If the HIDE_UNDOC_CLASSES tag is set to YES, doxygen will hide all +# undocumented classes that are normally visible in the class hierarchy. If set +# to NO these classes will be included in the various overviews. This option has +# no effect if EXTRACT_ALL is enabled. +# The default value is: NO. + +HIDE_UNDOC_CLASSES = NO + +# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, doxygen will hide all friend +# (class|struct|union) declarations. If set to NO these declarations will be +# included in the documentation. +# The default value is: NO. + +HIDE_FRIEND_COMPOUNDS = NO + +# If the HIDE_IN_BODY_DOCS tag is set to YES, doxygen will hide any +# documentation blocks found inside the body of a function. If set to NO these +# blocks will be appended to the function's detailed documentation block. +# The default value is: NO. + +HIDE_IN_BODY_DOCS = NO + +# The INTERNAL_DOCS tag determines if documentation that is typed after a +# \internal command is included. If the tag is set to NO then the documentation +# will be excluded. Set it to YES to include the internal documentation. +# The default value is: NO. + +INTERNAL_DOCS = NO + +# If the CASE_SENSE_NAMES tag is set to NO then doxygen will only generate file +# names in lower-case letters. If set to YES upper-case letters are also +# allowed. This is useful if you have classes or files whose names only differ +# in case and if your file system supports case sensitive file names. Windows +# and Mac users are advised to set this option to NO. +# The default value is: system dependent. + +CASE_SENSE_NAMES = YES + +# If the HIDE_SCOPE_NAMES tag is set to NO then doxygen will show members with +# their full class and namespace scopes in the documentation. If set to YES the +# scope will be hidden. +# The default value is: NO. + +HIDE_SCOPE_NAMES = NO + +# If the SHOW_INCLUDE_FILES tag is set to YES then doxygen will put a list of +# the files that are included by a file in the documentation of that file. +# The default value is: YES. + +SHOW_INCLUDE_FILES = YES + +# If the SHOW_GROUPED_MEMB_INC tag is set to YES then Doxygen will add for each +# grouped member an include statement to the documentation, telling the reader +# which file to include in order to use the member. +# The default value is: NO. + +SHOW_GROUPED_MEMB_INC = NO + +# If the FORCE_LOCAL_INCLUDES tag is set to YES then doxygen will list include +# files with double quotes in the documentation rather than with sharp brackets. +# The default value is: NO. + +FORCE_LOCAL_INCLUDES = NO + +# If the INLINE_INFO tag is set to YES then a tag [inline] is inserted in the +# documentation for inline members. +# The default value is: YES. + +INLINE_INFO = YES + +# If the SORT_MEMBER_DOCS tag is set to YES then doxygen will sort the +# (detailed) documentation of file and class members alphabetically by member +# name. If set to NO the members will appear in declaration order. +# The default value is: YES. + +SORT_MEMBER_DOCS = YES + +# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the brief +# descriptions of file, namespace and class members alphabetically by member +# name. If set to NO the members will appear in declaration order. Note that +# this will also influence the order of the classes in the class list. +# The default value is: NO. + +SORT_BRIEF_DOCS = NO + +# If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen will sort the +# (brief and detailed) documentation of class members so that constructors and +# destructors are listed first. If set to NO the constructors will appear in the +# respective orders defined by SORT_BRIEF_DOCS and SORT_MEMBER_DOCS. +# Note: If SORT_BRIEF_DOCS is set to NO this option is ignored for sorting brief +# member documentation. +# Note: If SORT_MEMBER_DOCS is set to NO this option is ignored for sorting +# detailed member documentation. +# The default value is: NO. + +SORT_MEMBERS_CTORS_1ST = NO + +# If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the hierarchy +# of group names into alphabetical order. If set to NO the group names will +# appear in their defined order. +# The default value is: NO. + +SORT_GROUP_NAMES = NO + +# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be sorted by +# fully-qualified names, including namespaces. If set to NO, the class list will +# be sorted only by class name, not including the namespace part. +# Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES. +# Note: This option applies only to the class list, not to the alphabetical +# list. +# The default value is: NO. + +SORT_BY_SCOPE_NAME = NO + +# If the STRICT_PROTO_MATCHING option is enabled and doxygen fails to do proper +# type resolution of all parameters of a function it will reject a match between +# the prototype and the implementation of a member function even if there is +# only one candidate or it is obvious which candidate to choose by doing a +# simple string match. By disabling STRICT_PROTO_MATCHING doxygen will still +# accept a match between prototype and implementation in such cases. +# The default value is: NO. + +STRICT_PROTO_MATCHING = NO + +# The GENERATE_TODOLIST tag can be used to enable ( YES) or disable ( NO) the +# todo list. This list is created by putting \todo commands in the +# documentation. +# The default value is: YES. + +GENERATE_TODOLIST = YES + +# The GENERATE_TESTLIST tag can be used to enable ( YES) or disable ( NO) the +# test list. This list is created by putting \test commands in the +# documentation. +# The default value is: YES. + +GENERATE_TESTLIST = YES + +# The GENERATE_BUGLIST tag can be used to enable ( YES) or disable ( NO) the bug +# list. This list is created by putting \bug commands in the documentation. +# The default value is: YES. + +GENERATE_BUGLIST = YES + +# The GENERATE_DEPRECATEDLIST tag can be used to enable ( YES) or disable ( NO) +# the deprecated list. This list is created by putting \deprecated commands in +# the documentation. +# The default value is: YES. + +GENERATE_DEPRECATEDLIST= YES + +# The ENABLED_SECTIONS tag can be used to enable conditional documentation +# sections, marked by \if ... \endif and \cond +# ... \endcond blocks. + +ENABLED_SECTIONS = + +# The MAX_INITIALIZER_LINES tag determines the maximum number of lines that the +# initial value of a variable or macro / define can have for it to appear in the +# documentation. If the initializer consists of more lines than specified here +# it will be hidden. Use a value of 0 to hide initializers completely. The +# appearance of the value of individual variables and macros / defines can be +# controlled using \showinitializer or \hideinitializer command in the +# documentation regardless of this setting. +# Minimum value: 0, maximum value: 10000, default value: 30. + +MAX_INITIALIZER_LINES = 30 + +# Set the SHOW_USED_FILES tag to NO to disable the list of files generated at +# the bottom of the documentation of classes and structs. If set to YES the list +# will mention the files that were used to generate the documentation. +# The default value is: YES. + +SHOW_USED_FILES = YES + +# Set the SHOW_FILES tag to NO to disable the generation of the Files page. This +# will remove the Files entry from the Quick Index and from the Folder Tree View +# (if specified). +# The default value is: YES. + +SHOW_FILES = YES + +# Set the SHOW_NAMESPACES tag to NO to disable the generation of the Namespaces +# page. This will remove the Namespaces entry from the Quick Index and from the +# Folder Tree View (if specified). +# The default value is: YES. + +SHOW_NAMESPACES = YES + +# The FILE_VERSION_FILTER tag can be used to specify a program or script that +# doxygen should invoke to get the current version for each file (typically from +# the version control system). Doxygen will invoke the program by executing (via +# popen()) the command command input-file, where command is the value of the +# FILE_VERSION_FILTER tag, and input-file is the name of an input file provided +# by doxygen. Whatever the program writes to standard output is used as the file +# version. For an example see the documentation. + +FILE_VERSION_FILTER = + +# The LAYOUT_FILE tag can be used to specify a layout file which will be parsed +# by doxygen. The layout file controls the global structure of the generated +# output files in an output format independent way. To create the layout file +# that represents doxygen's defaults, run doxygen with the -l option. You can +# optionally specify a file name after the option, if omitted DoxygenLayout.xml +# will be used as the name of the layout file. +# +# Note that if you run doxygen from a directory containing a file called +# DoxygenLayout.xml, doxygen will parse it automatically even if the LAYOUT_FILE +# tag is left empty. + +LAYOUT_FILE = + +# The CITE_BIB_FILES tag can be used to specify one or more bib files containing +# the reference definitions. This must be a list of .bib files. The .bib +# extension is automatically appended if omitted. This requires the bibtex tool +# to be installed. See also http://en.wikipedia.org/wiki/BibTeX for more info. +# For LaTeX the style of the bibliography can be controlled using +# LATEX_BIB_STYLE. To use this feature you need bibtex and perl available in the +# search path. Do not use file names with spaces, bibtex cannot handle them. See +# also \cite for info how to create references. + +CITE_BIB_FILES = + +#--------------------------------------------------------------------------- +# Configuration options related to warning and progress messages +#--------------------------------------------------------------------------- + +# The QUIET tag can be used to turn on/off the messages that are generated to +# standard output by doxygen. If QUIET is set to YES this implies that the +# messages are off. +# The default value is: NO. + +QUIET = NO + +# The WARNINGS tag can be used to turn on/off the warning messages that are +# generated to standard error ( stderr) by doxygen. If WARNINGS is set to YES +# this implies that the warnings are on. +# +# Tip: Turn warnings on while writing the documentation. +# The default value is: YES. + +WARNINGS = YES + +# If the WARN_IF_UNDOCUMENTED tag is set to YES, then doxygen will generate +# warnings for undocumented members. If EXTRACT_ALL is set to YES then this flag +# will automatically be disabled. +# The default value is: YES. + +WARN_IF_UNDOCUMENTED = NO + +# If the WARN_IF_DOC_ERROR tag is set to YES, doxygen will generate warnings for +# potential errors in the documentation, such as not documenting some parameters +# in a documented function, or documenting parameters that don't exist or using +# markup commands wrongly. +# The default value is: YES. + +WARN_IF_DOC_ERROR = YES + +# This WARN_NO_PARAMDOC option can be enabled to get warnings for functions that +# are documented, but have no documentation for their parameters or return +# value. If set to NO doxygen will only warn about wrong or incomplete parameter +# documentation, but not about the absence of documentation. +# The default value is: NO. + +WARN_NO_PARAMDOC = NO + +# The WARN_FORMAT tag determines the format of the warning messages that doxygen +# can produce. The string should contain the $file, $line, and $text tags, which +# will be replaced by the file and line number from which the warning originated +# and the warning text. Optionally the format may contain $version, which will +# be replaced by the version of the file (if it could be obtained via +# FILE_VERSION_FILTER) +# The default value is: $file:$line: $text. + +WARN_FORMAT = "$file:$line: $text" + +# The WARN_LOGFILE tag can be used to specify a file to which warning and error +# messages should be written. If left blank the output is written to standard +# error (stderr). + +WARN_LOGFILE = + +#--------------------------------------------------------------------------- +# Configuration options related to the input files +#--------------------------------------------------------------------------- + +# The INPUT tag is used to specify the files and/or directories that contain +# documented source files. You may enter file names like myfile.cpp or +# directories like /usr/src/myproject. Separate the files or directories with +# spaces. +# Note: If this tag is empty the current directory is searched. + +INPUT = @OpenMW_SOURCE_DIR@/apps \ + @OpenMW_SOURCE_DIR@/components \ + @OpenMW_SOURCE_DIR@/libs \ + @OpenMW_BINARY_DIR@/docs/mainpage.hpp + +# This tag can be used to specify the character encoding of the source files +# that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses +# libiconv (or the iconv built into libc) for the transcoding. See the libiconv +# documentation (see: http://www.gnu.org/software/libiconv) for the list of +# possible encodings. +# The default value is: UTF-8. + +INPUT_ENCODING = UTF-8 + +# If the value of the INPUT tag contains directories, you can use the +# FILE_PATTERNS tag to specify one or more wildcard patterns (like *.cpp and +# *.h) to filter out the source-files in the directories. If left blank the +# following patterns are tested:*.c, *.cc, *.cxx, *.cpp, *.c++, *.java, *.ii, +# *.ixx, *.ipp, *.i++, *.inl, *.idl, *.ddl, *.odl, *.h, *.hh, *.hxx, *.hpp, +# *.h++, *.cs, *.d, *.php, *.php4, *.php5, *.phtml, *.inc, *.m, *.markdown, +# *.md, *.mm, *.dox, *.py, *.f90, *.f, *.for, *.tcl, *.vhd, *.vhdl, *.ucf, +# *.qsf, *.as and *.js. + +FILE_PATTERNS = *.c \ + *.cc \ + *.cxx \ + *.cpp \ + *.c++ \ + *.java \ + *.ii \ + *.ixx \ + *.ipp \ + *.i++ \ + *.inl \ + *.h \ + *.hh \ + *.hxx \ + *.hpp \ + *.h++ \ + *.idl \ + *.odl \ + *.cs \ + *.php \ + *.php3 \ + *.inc \ + *.m \ + *.mm \ + *.dox \ + *.py \ + *.f90 \ + *.f \ + *.vhd \ + *.vhdl + +# The RECURSIVE tag can be used to specify whether or not subdirectories should +# be searched for input files as well. +# The default value is: NO. + +RECURSIVE = YES + +# The EXCLUDE tag can be used to specify files and/or directories that should be +# excluded from the INPUT source files. This way you can easily exclude a +# subdirectory from a directory tree whose root is specified with the INPUT tag. +# +# Note that relative paths are relative to the directory from which doxygen is +# run. + +EXCLUDE = + +# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or +# directories that are symbolic links (a Unix file system feature) are excluded +# from the input. +# The default value is: NO. + +EXCLUDE_SYMLINKS = NO + +# If the value of the INPUT tag contains directories, you can use the +# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude +# certain files from those directories. +# +# Note that the wildcards are matched against the file with absolute path, so to +# exclude all test directories for example use the pattern */test/* + +EXCLUDE_PATTERNS = + +# The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names +# (namespaces, classes, functions, etc.) that should be excluded from the +# output. The symbol name can be a fully qualified name, a word, or if the +# wildcard * is used, a substring. Examples: ANamespace, AClass, +# AClass::ANamespace, ANamespace::*Test +# +# Note that the wildcards are matched against the file with absolute path, so to +# exclude all test directories use the pattern */test/* + +EXCLUDE_SYMBOLS = + +# The EXAMPLE_PATH tag can be used to specify one or more files or directories +# that contain example code fragments that are included (see the \include +# command). + +EXAMPLE_PATH = + +# If the value of the EXAMPLE_PATH tag contains directories, you can use the +# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp and +# *.h) to filter out the source-files in the directories. If left blank all +# files are included. + +EXAMPLE_PATTERNS = * + +# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be +# searched for input files to be used with the \include or \dontinclude commands +# irrespective of the value of the RECURSIVE tag. +# The default value is: NO. + +EXAMPLE_RECURSIVE = NO + +# The IMAGE_PATH tag can be used to specify one or more files or directories +# that contain images that are to be included in the documentation (see the +# \image command). + +IMAGE_PATH = + +# The INPUT_FILTER tag can be used to specify a program that doxygen should +# invoke to filter for each input file. Doxygen will invoke the filter program +# by executing (via popen()) the command: +# +# +# +# where is the value of the INPUT_FILTER tag, and is the +# name of an input file. Doxygen will then use the output that the filter +# program writes to standard output. If FILTER_PATTERNS is specified, this tag +# will be ignored. +# +# Note that the filter must not add or remove lines; it is applied before the +# code is scanned, but not when the output code is generated. If lines are added +# or removed, the anchors will not be placed correctly. + +INPUT_FILTER = + +# The FILTER_PATTERNS tag can be used to specify filters on a per file pattern +# basis. Doxygen will compare the file name with each pattern and apply the +# filter if there is a match. The filters are a list of the form: pattern=filter +# (like *.cpp=my_cpp_filter). See INPUT_FILTER for further information on how +# filters are used. If the FILTER_PATTERNS tag is empty or if none of the +# patterns match the file name, INPUT_FILTER is applied. + +FILTER_PATTERNS = + +# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using +# INPUT_FILTER ) will also be used to filter the input files that are used for +# producing the source files to browse (i.e. when SOURCE_BROWSER is set to YES). +# The default value is: NO. + +FILTER_SOURCE_FILES = NO + +# The FILTER_SOURCE_PATTERNS tag can be used to specify source filters per file +# pattern. A pattern will override the setting for FILTER_PATTERN (if any) and +# it is also possible to disable source filtering for a specific pattern using +# *.ext= (so without naming a filter). +# This tag requires that the tag FILTER_SOURCE_FILES is set to YES. + +FILTER_SOURCE_PATTERNS = + +# If the USE_MDFILE_AS_MAINPAGE tag refers to the name of a markdown file that +# is part of the input, its contents will be placed on the main page +# (index.html). This can be useful if you have a project on for instance GitHub +# and want to reuse the introduction page also for the doxygen output. + +USE_MDFILE_AS_MAINPAGE = + +#--------------------------------------------------------------------------- +# Configuration options related to source browsing +#--------------------------------------------------------------------------- + +# If the SOURCE_BROWSER tag is set to YES then a list of source files will be +# generated. Documented entities will be cross-referenced with these sources. +# +# Note: To get rid of all source code in the generated output, make sure that +# also VERBATIM_HEADERS is set to NO. +# The default value is: NO. + +SOURCE_BROWSER = NO + +# Setting the INLINE_SOURCES tag to YES will include the body of functions, +# classes and enums directly into the documentation. +# The default value is: NO. + +INLINE_SOURCES = NO + +# Setting the STRIP_CODE_COMMENTS tag to YES will instruct doxygen to hide any +# special comment blocks from generated source code fragments. Normal C, C++ and +# Fortran comments will always remain visible. +# The default value is: YES. + +STRIP_CODE_COMMENTS = YES + +# If the REFERENCED_BY_RELATION tag is set to YES then for each documented +# function all documented functions referencing it will be listed. +# The default value is: NO. + +REFERENCED_BY_RELATION = NO + +# If the REFERENCES_RELATION tag is set to YES then for each documented function +# all documented entities called/used by that function will be listed. +# The default value is: NO. + +REFERENCES_RELATION = NO + +# If the REFERENCES_LINK_SOURCE tag is set to YES and SOURCE_BROWSER tag is set +# to YES, then the hyperlinks from functions in REFERENCES_RELATION and +# REFERENCED_BY_RELATION lists will link to the source code. Otherwise they will +# link to the documentation. +# The default value is: YES. + +REFERENCES_LINK_SOURCE = YES + +# If SOURCE_TOOLTIPS is enabled (the default) then hovering a hyperlink in the +# source code will show a tooltip with additional information such as prototype, +# brief description and links to the definition and documentation. Since this +# will make the HTML file larger and loading of large files a bit slower, you +# can opt to disable this feature. +# The default value is: YES. +# This tag requires that the tag SOURCE_BROWSER is set to YES. + +SOURCE_TOOLTIPS = YES + +# If the USE_HTAGS tag is set to YES then the references to source code will +# point to the HTML generated by the htags(1) tool instead of doxygen built-in +# source browser. The htags tool is part of GNU's global source tagging system +# (see http://www.gnu.org/software/global/global.html). You will need version +# 4.8.6 or higher. +# +# To use it do the following: +# - Install the latest version of global +# - Enable SOURCE_BROWSER and USE_HTAGS in the config file +# - Make sure the INPUT points to the root of the source tree +# - Run doxygen as normal +# +# Doxygen will invoke htags (and that will in turn invoke gtags), so these +# tools must be available from the command line (i.e. in the search path). +# +# The result: instead of the source browser generated by doxygen, the links to +# source code will now point to the output of htags. +# The default value is: NO. +# This tag requires that the tag SOURCE_BROWSER is set to YES. + +USE_HTAGS = NO + +# If the VERBATIM_HEADERS tag is set the YES then doxygen will generate a +# verbatim copy of the header file for each class for which an include is +# specified. Set to NO to disable this. +# See also: Section \class. +# The default value is: YES. + +VERBATIM_HEADERS = YES + +#--------------------------------------------------------------------------- +# Configuration options related to the alphabetical class index +#--------------------------------------------------------------------------- + +# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index of all +# compounds will be generated. Enable this if the project contains a lot of +# classes, structs, unions or interfaces. +# The default value is: YES. + +ALPHABETICAL_INDEX = NO + +# The COLS_IN_ALPHA_INDEX tag can be used to specify the number of columns in +# which the alphabetical index list will be split. +# Minimum value: 1, maximum value: 20, default value: 5. +# This tag requires that the tag ALPHABETICAL_INDEX is set to YES. + +COLS_IN_ALPHA_INDEX = 5 + +# In case all classes in a project start with a common prefix, all classes will +# be put under the same header in the alphabetical index. The IGNORE_PREFIX tag +# can be used to specify a prefix (or a list of prefixes) that should be ignored +# while generating the index headers. +# This tag requires that the tag ALPHABETICAL_INDEX is set to YES. + +IGNORE_PREFIX = + +#--------------------------------------------------------------------------- +# Configuration options related to the HTML output +#--------------------------------------------------------------------------- + +# If the GENERATE_HTML tag is set to YES doxygen will generate HTML output +# The default value is: YES. + +GENERATE_HTML = YES + +# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. If a +# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of +# it. +# The default directory is: html. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_OUTPUT = html + +# The HTML_FILE_EXTENSION tag can be used to specify the file extension for each +# generated HTML page (for example: .htm, .php, .asp). +# The default value is: .html. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_FILE_EXTENSION = .html + +# The HTML_HEADER tag can be used to specify a user-defined HTML header file for +# each generated HTML page. If the tag is left blank doxygen will generate a +# standard header. +# +# To get valid HTML the header file that includes any scripts and style sheets +# that doxygen needs, which is dependent on the configuration options used (e.g. +# the setting GENERATE_TREEVIEW). It is highly recommended to start with a +# default header using +# doxygen -w html new_header.html new_footer.html new_stylesheet.css +# YourConfigFile +# and then modify the file new_header.html. See also section "Doxygen usage" +# for information on how to generate the default header that doxygen normally +# uses. +# Note: The header is subject to change so you typically have to regenerate the +# default header when upgrading to a newer version of doxygen. For a description +# of the possible markers and block names see the documentation. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_HEADER = + +# The HTML_FOOTER tag can be used to specify a user-defined HTML footer for each +# generated HTML page. If the tag is left blank doxygen will generate a standard +# footer. See HTML_HEADER for more information on how to generate a default +# footer and what special commands can be used inside the footer. See also +# section "Doxygen usage" for information on how to generate the default footer +# that doxygen normally uses. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_FOOTER = + +# The HTML_STYLESHEET tag can be used to specify a user-defined cascading style +# sheet that is used by each HTML page. It can be used to fine-tune the look of +# the HTML output. If left blank doxygen will generate a default style sheet. +# See also section "Doxygen usage" for information on how to generate the style +# sheet that doxygen normally uses. +# Note: It is recommended to use HTML_EXTRA_STYLESHEET instead of this tag, as +# it is more robust and this tag (HTML_STYLESHEET) will in the future become +# obsolete. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_STYLESHEET = + +# The HTML_EXTRA_STYLESHEET tag can be used to specify an additional user- +# defined cascading style sheet that is included after the standard style sheets +# created by doxygen. Using this option one can overrule certain style aspects. +# This is preferred over using HTML_STYLESHEET since it does not replace the +# standard style sheet and is therefor more robust against future updates. +# Doxygen will copy the style sheet file to the output directory. For an example +# see the documentation. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_EXTRA_STYLESHEET = + +# The HTML_EXTRA_FILES tag can be used to specify one or more extra images or +# other source files which should be copied to the HTML output directory. Note +# that these files will be copied to the base HTML output directory. Use the +# $relpath^ marker in the HTML_HEADER and/or HTML_FOOTER files to load these +# files. In the HTML_STYLESHEET file, use the file name only. Also note that the +# files will be copied as-is; there are no commands or markers available. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_EXTRA_FILES = + +# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen +# will adjust the colors in the stylesheet and background images according to +# this color. Hue is specified as an angle on a colorwheel, see +# http://en.wikipedia.org/wiki/Hue for more information. For instance the value +# 0 represents red, 60 is yellow, 120 is green, 180 is cyan, 240 is blue, 300 +# purple, and 360 is red again. +# Minimum value: 0, maximum value: 359, default value: 220. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE_HUE = 220 + +# The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of the colors +# in the HTML output. For a value of 0 the output will use grayscales only. A +# value of 255 will produce the most vivid colors. +# Minimum value: 0, maximum value: 255, default value: 100. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE_SAT = 100 + +# The HTML_COLORSTYLE_GAMMA tag controls the gamma correction applied to the +# luminance component of the colors in the HTML output. Values below 100 +# gradually make the output lighter, whereas values above 100 make the output +# darker. The value divided by 100 is the actual gamma applied, so 80 represents +# a gamma of 0.8, The value 220 represents a gamma of 2.2, and 100 does not +# change the gamma. +# Minimum value: 40, maximum value: 240, default value: 80. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE_GAMMA = 80 + +# If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML +# page will contain the date and time when the page was generated. Setting this +# to NO can help when comparing the output of multiple runs. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_TIMESTAMP = YES + +# If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML +# documentation will contain sections that can be hidden and shown after the +# page has loaded. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_DYNAMIC_SECTIONS = NO + +# With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries +# shown in the various tree structured indices initially; the user can expand +# and collapse entries dynamically later on. Doxygen will expand the tree to +# such a level that at most the specified number of entries are visible (unless +# a fully collapsed tree already exceeds this amount). So setting the number of +# entries 1 will produce a full collapsed tree by default. 0 is a special value +# representing an infinite number of entries and will result in a full expanded +# tree by default. +# Minimum value: 0, maximum value: 9999, default value: 100. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_INDEX_NUM_ENTRIES = 100 + +# If the GENERATE_DOCSET tag is set to YES, additional index files will be +# generated that can be used as input for Apple's Xcode 3 integrated development +# environment (see: http://developer.apple.com/tools/xcode/), introduced with +# OSX 10.5 (Leopard). To create a documentation set, doxygen will generate a +# Makefile in the HTML output directory. Running make will produce the docset in +# that directory and running make install will install the docset in +# ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find it at +# startup. See http://developer.apple.com/tools/creatingdocsetswithdoxygen.html +# for more information. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_DOCSET = NO + +# This tag determines the name of the docset feed. A documentation feed provides +# an umbrella under which multiple documentation sets from a single provider +# (such as a company or product suite) can be grouped. +# The default value is: Doxygen generated docs. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_FEEDNAME = "OpenMW Documentation" + +# This tag specifies a string that should uniquely identify the documentation +# set bundle. This should be a reverse domain-name style string, e.g. +# com.mycompany.MyDocSet. Doxygen will append .docset to the name. +# The default value is: org.doxygen.Project. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_BUNDLE_ID = org.openmw + +# The DOCSET_PUBLISHER_ID tag specifies a string that should uniquely identify +# the documentation publisher. This should be a reverse domain-name style +# string, e.g. com.mycompany.MyDocSet.documentation. +# The default value is: org.doxygen.Publisher. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_PUBLISHER_ID = org.openmw + +# The DOCSET_PUBLISHER_NAME tag identifies the documentation publisher. +# The default value is: Publisher. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_PUBLISHER_NAME = OpenMW + +# If the GENERATE_HTMLHELP tag is set to YES then doxygen generates three +# additional HTML index files: index.hhp, index.hhc, and index.hhk. The +# index.hhp is a project file that can be read by Microsoft's HTML Help Workshop +# (see: http://www.microsoft.com/en-us/download/details.aspx?id=21138) on +# Windows. +# +# The HTML Help Workshop contains a compiler that can convert all HTML output +# generated by doxygen into a single compiled HTML file (.chm). Compiled HTML +# files are now used as the Windows 98 help format, and will replace the old +# Windows help format (.hlp) on all Windows platforms in the future. Compressed +# HTML files also contain an index, a table of contents, and you can search for +# words in the documentation. The HTML workshop also contains a viewer for +# compressed HTML files. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_HTMLHELP = NO + +# The CHM_FILE tag can be used to specify the file name of the resulting .chm +# file. You can add a path in front of the file if the result should not be +# written to the html output directory. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +CHM_FILE = + +# The HHC_LOCATION tag can be used to specify the location (absolute path +# including file name) of the HTML help compiler ( hhc.exe). If non-empty +# doxygen will try to run the HTML help compiler on the generated index.hhp. +# The file has to be specified with full path. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +HHC_LOCATION = + +# The GENERATE_CHI flag controls if a separate .chi index file is generated ( +# YES) or that it should be included in the master .chm file ( NO). +# The default value is: NO. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +GENERATE_CHI = NO + +# The CHM_INDEX_ENCODING is used to encode HtmlHelp index ( hhk), content ( hhc) +# and project file content. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +CHM_INDEX_ENCODING = + +# The BINARY_TOC flag controls whether a binary table of contents is generated ( +# YES) or a normal table of contents ( NO) in the .chm file. Furthermore it +# enables the Previous and Next buttons. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +BINARY_TOC = NO + +# The TOC_EXPAND flag can be set to YES to add extra items for group members to +# the table of contents of the HTML help documentation and to the tree view. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +TOC_EXPAND = NO + +# If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and +# QHP_VIRTUAL_FOLDER are set, an additional index file will be generated that +# can be used as input for Qt's qhelpgenerator to generate a Qt Compressed Help +# (.qch) of the generated HTML documentation. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_QHP = NO + +# If the QHG_LOCATION tag is specified, the QCH_FILE tag can be used to specify +# the file name of the resulting .qch file. The path specified is relative to +# the HTML output folder. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QCH_FILE = + +# The QHP_NAMESPACE tag specifies the namespace to use when generating Qt Help +# Project output. For more information please see Qt Help Project / Namespace +# (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#namespace). +# The default value is: org.doxygen.Project. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_NAMESPACE = org.openmw + +# The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating Qt +# Help Project output. For more information please see Qt Help Project / Virtual +# Folders (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#virtual- +# folders). +# The default value is: doc. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_VIRTUAL_FOLDER = doc + +# If the QHP_CUST_FILTER_NAME tag is set, it specifies the name of a custom +# filter to add. For more information please see Qt Help Project / Custom +# Filters (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#custom- +# filters). +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_CUST_FILTER_NAME = + +# The QHP_CUST_FILTER_ATTRS tag specifies the list of the attributes of the +# custom filter to add. For more information please see Qt Help Project / Custom +# Filters (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#custom- +# filters). +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_CUST_FILTER_ATTRS = + +# The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this +# project's filter section matches. Qt Help Project / Filter Attributes (see: +# http://qt-project.org/doc/qt-4.8/qthelpproject.html#filter-attributes). +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_SECT_FILTER_ATTRS = + +# The QHG_LOCATION tag can be used to specify the location of Qt's +# qhelpgenerator. If non-empty doxygen will try to run qhelpgenerator on the +# generated .qhp file. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHG_LOCATION = + +# If the GENERATE_ECLIPSEHELP tag is set to YES, additional index files will be +# generated, together with the HTML files, they form an Eclipse help plugin. To +# install this plugin and make it available under the help contents menu in +# Eclipse, the contents of the directory containing the HTML and XML files needs +# to be copied into the plugins directory of eclipse. The name of the directory +# within the plugins directory should be the same as the ECLIPSE_DOC_ID value. +# After copying Eclipse needs to be restarted before the help appears. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_ECLIPSEHELP = NO + +# A unique identifier for the Eclipse help plugin. When installing the plugin +# the directory name containing the HTML and XML files should also have this +# name. Each documentation set should have its own identifier. +# The default value is: org.doxygen.Project. +# This tag requires that the tag GENERATE_ECLIPSEHELP is set to YES. + +ECLIPSE_DOC_ID = org.openmw + +# If you want full control over the layout of the generated HTML pages it might +# be necessary to disable the index and replace it with your own. The +# DISABLE_INDEX tag can be used to turn on/off the condensed index (tabs) at top +# of each HTML page. A value of NO enables the index and the value YES disables +# it. Since the tabs in the index contain the same information as the navigation +# tree, you can set this option to YES if you also set GENERATE_TREEVIEW to YES. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +DISABLE_INDEX = NO + +# The GENERATE_TREEVIEW tag is used to specify whether a tree-like index +# structure should be generated to display hierarchical information. If the tag +# value is set to YES, a side panel will be generated containing a tree-like +# index structure (just like the one that is generated for HTML Help). For this +# to work a browser that supports JavaScript, DHTML, CSS and frames is required +# (i.e. any modern browser). Windows users are probably better off using the +# HTML help feature. Via custom stylesheets (see HTML_EXTRA_STYLESHEET) one can +# further fine-tune the look of the index. As an example, the default style +# sheet generated by doxygen has an example that shows how to put an image at +# the root of the tree instead of the PROJECT_NAME. Since the tree basically has +# the same information as the tab index, you could consider setting +# DISABLE_INDEX to YES when enabling this option. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_TREEVIEW = NO + +# The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values that +# doxygen will group on one line in the generated HTML documentation. +# +# Note that a value of 0 will completely suppress the enum values from appearing +# in the overview section. +# Minimum value: 0, maximum value: 20, default value: 4. +# This tag requires that the tag GENERATE_HTML is set to YES. + +ENUM_VALUES_PER_LINE = 4 + +# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be used +# to set the initial width (in pixels) of the frame in which the tree is shown. +# Minimum value: 0, maximum value: 1500, default value: 250. +# This tag requires that the tag GENERATE_HTML is set to YES. + +TREEVIEW_WIDTH = 250 + +# When the EXT_LINKS_IN_WINDOW option is set to YES doxygen will open links to +# external symbols imported via tag files in a separate window. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +EXT_LINKS_IN_WINDOW = NO + +# Use this tag to change the font size of LaTeX formulas included as images in +# the HTML documentation. When you change the font size after a successful +# doxygen run you need to manually remove any form_*.png images from the HTML +# output directory to force them to be regenerated. +# Minimum value: 8, maximum value: 50, default value: 10. +# This tag requires that the tag GENERATE_HTML is set to YES. + +FORMULA_FONTSIZE = 10 + +# Use the FORMULA_TRANPARENT tag to determine whether or not the images +# generated for formulas are transparent PNGs. Transparent PNGs are not +# supported properly for IE 6.0, but are supported on all modern browsers. +# +# Note that when changing this option you need to delete any form_*.png files in +# the HTML output directory before the changes have effect. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +FORMULA_TRANSPARENT = YES + +# Enable the USE_MATHJAX option to render LaTeX formulas using MathJax (see +# http://www.mathjax.org) which uses client side Javascript for the rendering +# instead of using prerendered bitmaps. Use this if you do not have LaTeX +# installed or if you want to formulas look prettier in the HTML output. When +# enabled you may also need to install MathJax separately and configure the path +# to it using the MATHJAX_RELPATH option. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +USE_MATHJAX = YES + +# When MathJax is enabled you can set the default output format to be used for +# the MathJax output. See the MathJax site (see: +# http://docs.mathjax.org/en/latest/output.html) for more details. +# Possible values are: HTML-CSS (which is slower, but has the best +# compatibility), NativeMML (i.e. MathML) and SVG. +# The default value is: HTML-CSS. +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_FORMAT = HTML-CSS + +# When MathJax is enabled you need to specify the location relative to the HTML +# output directory using the MATHJAX_RELPATH option. The destination directory +# should contain the MathJax.js script. For instance, if the mathjax directory +# is located at the same level as the HTML output directory, then +# MATHJAX_RELPATH should be ../mathjax. The default value points to the MathJax +# Content Delivery Network so you can quickly see the result without installing +# MathJax. However, it is strongly recommended to install a local copy of +# MathJax from http://www.mathjax.org before deployment. +# The default value is: http://cdn.mathjax.org/mathjax/latest. +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_RELPATH = http://cdn.mathjax.org/mathjax/latest + +# The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax +# extension names that should be enabled during MathJax rendering. For example +# MATHJAX_EXTENSIONS = TeX/AMSmath TeX/AMSsymbols +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_EXTENSIONS = + +# The MATHJAX_CODEFILE tag can be used to specify a file with javascript pieces +# of code that will be used on startup of the MathJax code. See the MathJax site +# (see: http://docs.mathjax.org/en/latest/output.html) for more details. For an +# example see the documentation. +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_CODEFILE = + +# When the SEARCHENGINE tag is enabled doxygen will generate a search box for +# the HTML output. The underlying search engine uses javascript and DHTML and +# should work on any modern browser. Note that when using HTML help +# (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets (GENERATE_DOCSET) +# there is already a search function so this one should typically be disabled. +# For large projects the javascript based search engine can be slow, then +# enabling SERVER_BASED_SEARCH may provide a better solution. It is possible to +# search using the keyboard; to jump to the search box use + S +# (what the is depends on the OS and browser, but it is typically +# , /